Java’s Thread Class in Five Minutes

Share this article

Java’s Thread Class in Five Minutes

Key Takeaways

  • In Java, all code is executed in threads; within a thread, everything happens sequentially. New threads can be created to execute code in parallel using the Thread class.
  • To create a thread, a block of code needs to be defined by implementing the Runnable interface. An instance of this implementation can then be passed to the Thread’s constructor.
  • A thread finishes when all instructions in the Runnable are executed or when an uncaught exception is thrown from the run method. Other threads will continue their execution independently if a thread fails.
  • Threads can interact with each other, potentially causing unexpected issues. To mitigate this, concepts like race conditions, synchronization, locks, and concurrent collections should be explored. Thread is considered a low-level tool in multithreaded programming; using thread pools and executors is often recommended.

Table of Contents

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.

Parallel Threads

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.

Frequently Asked Questions (FAQs) about Java Thread Class

What is the main difference between implementing Runnable and extending Thread in Java?

When you extend the Thread class, you’re actually creating a new thread and providing the code it should run. On the other hand, implementing Runnable means you’re just providing the code that a thread should run. It’s more flexible because you can pass an instance of it to a Thread if you want to run it in a new thread, or you can just call run() directly if you don’t. It’s generally recommended to implement Runnable for this reason.

How can I stop a thread in Java?

In Java, you can’t forcibly stop a thread because it can lead to resource leaks. Instead, you should use a shared variable as a signal that tells the thread to stop running. The thread should check this variable regularly, and return from its run method if the variable indicates that it should stop.

What is the purpose of the join() method in Java threads?

The join() method allows one thread to wait for the completion of another. If t is a Thread object whose thread is currently executing, then t.join() will make sure that t is terminated before the next instruction is executed by the program.

How does thread priority influence thread behavior?

In Java, each thread is assigned a priority. Threads with higher priority are more important to a program and should be allocated processor time before lower-priority threads. However, thread priorities cannot guarantee the order in which threads execute and are very much platform dependent.

What is the difference between the sleep() and wait() methods in Java?

The main difference between sleep() and wait() is that sleep() is a static method that affects the currently executing thread, while wait() affects the thread that’s currently holding the lock on the object the wait() method is called from. Another key difference is that a sleeping thread will not release any locks it holds, while a waiting thread releases all locks it holds.

What is thread synchronization and why is it important?

Thread synchronization is defined as a mechanism which ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as critical section. It’s important because it prevents race conditions from occurring when multiple threads access shared resources.

What is the difference between a daemon thread and a user thread?

User threads are typically what you use to perform tasks in the foreground. Daemon threads are usually used to perform background tasks, such as garbage collection. The JVM will exit when all user threads finish executing, but it won’t wait for daemon threads to finish.

How can I make a thread execute periodically?

You can use the ScheduledExecutorService to schedule a task to run periodically. You can specify the initial delay and the period between subsequent executions.

What is thread pooling and why is it useful?

Thread pooling is a technique where you have a number of threads in a pool. When a task needs to be executed, a thread from the pool is chosen to perform the task. This is useful because creating and destroying threads can be expensive in terms of time and resources, so reusing existing threads can improve performance.

How can I handle exceptions in a separate thread?

You can use the UncaughtExceptionHandler to handle uncaught exceptions in threads. You can set it on a per-thread basis using the setUncaughtExceptionHandler method, or you can set it as the default handler for all threads using the static setDefaultUncaughtExceptionHandler method.

Ivan MushketykIvan Mushketyk
View Author

Passionate software developer interested in Java, Scala, and Big Data. Apache Flink contributor. Live in Dublin, Ireland.

concurrencynicolaipquick-tip
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week