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
.
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.
Passionate software developer interested in Java, Scala, and Big Data. Apache Flink contributor. Live in Dublin, Ireland.