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.