I wrote this article for SitePoint’s Java channel, where you can find a lot of interesting articles about our programming language. Check it out!
In a running Java program, all code is executed in threads and within a thread everything happens sequentially, one instruction after another. When Java (or rather the JVM) launches, it creates one thread for the main
method to be executed in. From there, new threads can be created to execute code in parallel to the main one. The most basic way to do that is to use the Thread
class.
This article does not require any knowledge of multithreaded programming, but you need to be familiar with core Java concepts such as classes and interfaces.
Thread Basics
Java offers a Thread
class that can be used to launch new threads, wait for them to finish, or interact with them in more advanced ways that go beyond the scope of this article.
Creating Threads
To create a thread, you need to define a block of code by implementing the Runnable
interface, which only has one abstract method run
. An instance of this implementation can then be passed to the Thread
‘s constructor.
Let’s start with an example that prints three messages and waits for half a second after each print.
class PrintingRunnable implements Runnable {
private final int id;
public PrintingRunnable(int id) {
this.id = id;
}
@Override
// This function will be executed in parallel
public void run() {
try {
// Print a message five times
for (int i = 0; i < 3; i++) {
System.out.println("Message " + i + " from Thread " + id);
// Wait for half a second (500ms)
Thread.sleep(500);
}
} catch (InterruptedException ex) {
System.out.println("Thread was interrupted");
}
}
}
The InterruptedException
is thrown by Thread.sleep()
. Handling it can be delicate but I won’t go into it here – you can read about it in this article.
We can use the Runnable
implementation to create a Thread
instance.
Thread thread = new Thread(new PrintingRunnable(1));
Launching Threads
With the thread in hand, it is time to launch it:
System.out.println("main() started");
Thread thread = new Thread(new PrintingRunnable(1));
thread.start();
System.out.println("main() finished");
If you run this code, you should see output similar to this:
main() started
Message 0 from Thread 1
main() finished
Message 1 from Thread 1
Message 2 from Thread 1
Notice that messages from the main
method and the thread we started are interleaving. This is because they run in parallel and their executions interleave unpredictably. In fact chances are, you will see slightly different output each time you run the program. At the same time, instructions within a single thread are always executed in the expected order as you can see from the increasing message numbers.
If you’ve only seen sequential Java code so far, you may be surprised that the program continued to run after the main
method has finished. In fact, the thread that is executing the main
method is not treated in any special way, and the JVM will finish the program execution once all threads are finished.
We can also start multiple threads with the same approach:
System.out.println("main() started");
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new PrintingRunnable(i));
thread.start();
}
System.out.println("main() finished");
In this case all three threads will output messages in parallel:
main() started
Message 0 from Thread 0
main() finished
Message 0 from Thread 1
Message 0 from Thread 2
Message 1 from Thread 0
Message 1 from Thread 2
Message 1 from Thread 1
Message 2 from Thread 1
Message 2 from Thread 0
Message 2 from Thread 2
Finishing Threads
So when does a thread finish? It happens in one of two cases:
- all instructions in the
Runnable
are executed - an uncaught exception is thrown from the
run
method
So far we have only encountered the first case. To see how the second option works I’ve implemented a Runnable
that prints a message and throws an exception:
class FailingRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread started");
// The compiler can detect when some code cannot be reached
// so this "if" statement is used to trick the compiler
// into letting me write a println after throwing
if (true) {
throw new RuntimeException("Stopping the thread");
}
System.out.println("This won't be printed");
}
}
Now let’s run this code in a separate thread:
Thread thread = new Thread(new FailingRunnable());
thread.start();
System.out.println("main() finished");
This is the output:
Thread started
Exception in thread "Thread-0" java.lang.RuntimeException: Stopping the thread
at com.example.FailingRunnable.run(App.java:58)
at java.lang.Thread.run(Thread.java:745)
main() finished
As you can see the last println
statement was not executed because the JVM stopped the thread’s execution once the exception was thrown.
You may be surprised that the main
function continued its execution despite the thrown exception. This is because different threads are independent of one another and if any of them fails others will continue as if nothing happened.
Waiting for Threads
One common task that we need to do with a thread is to wait until it is finished. In Java, this is pretty straightforward. All we need to do is to call the join
method on a thread instance:
System.out.println("main() started");
Thread thread = new Thread(new PrintingRunnable(1));
thread.start();
System.out.println("main() is waiting");
thread.join();
System.out.println("main() finished");
In this case, the calling thread will be blocked until the target thread is finished. When the last instruction in the target thread is executed the calling thread is resumed:
main() started
main() is waiting
Message 0 from Thread 1
Message 1 from Thread 1
Message 2 from Thread 1
main() finished
Notice that “main() finished” is printed after all messages from the PrintingThread
. Using the join
method this way we can ensure that some operations are executed strictly after all instructions in a particular thread. If we call join
on a thread that has already finished the call returns immediately and the calling thread is not being paused. This makes it easy to wait for several threads, just by looping over a collection of them and calling join
on each.
Because join
makes the calling thread pause, it also throws an InterruptedException
.
Conclusions
In this post. you have learned how to create independent threads of instructions in Java. To create a thread you need to implement the Runnable
interface and use an instance to create a Thread
object. Threads can be started with start
and its execution finishes when it runs out of instructions to execute or when it throws an uncaught exception. To wait until a thread’s execution is finished, we can use the join
method.
Usually threads interact with one another, which can cause some unexpected issues that don’t occur when writing single-threaded code. Explore topics like race conditions, synchronization, locks, and concurrent collections to learn more about this.
Nowadays, Thread
is considered to be a low-level tool in multithreaded programming. Instead of explicitly creating threads you might want to use thread pools that limit the number of threads in your application and executors that allow executing asynchronous tasks.
Share this: