Java Threads and Multithreading
Threads in Java
Java was originally designed with built-in multithreading support. Threads are supported at the level of the JVM, at the level of the language by special keywords, and at the level of the standard library, making them a key concept of the Java programming language. Every Java program has at least one thread, which is called main. It is created automatically by the JVM process to execute statements inside the main
method. All Java programs have some other default threads as well (for example, a separate thread for the garbage collector).
Throughout the stages of development of the Java language, the approach to multithreading has changed from the use of low-level threads to the use of some high-level abstractions. However, understanding the fundamental base remains very important for a good developer.
A class for threads
Each thread is represented by an object that is an instance of the java.lang.Thread
class (or its subclass). This class has a static method named currentThread
to obtain a reference to the currently executing thread object:
Thread thread = Thread.currentThread(); // the current thread
Any thread has a name, an identifier (long
), a priority, and some other characteristics that can be obtained through its methods.
The information about the main thread
The example below demonstrates how you can obtain the characteristics of the main thread by obtaining a reference to it through an object of the Thread
class.
public class MainThreadDemo {
public static void main(String[] args) {
Thread t = Thread.currentThread(); // main thread
System.out.println("Name: " + t.getName());
System.out.println("ID: " + t.getId());
System.out.println("Alive: " + t.isAlive());
System.out.println("Priority: " + t.getPriority());
System.out.println("Daemon: " + t.isDaemon());
t.setName("my-thread");
System.out.println("New name: " + t.getName());
}
}
All statements in this program are executed by the main thread.
The invocation t.isAlive()
returns a boolean which indicates whether the thread has been started and hasn't died yet. Every thread has a priority, and the getPriority()
method returns the priority of a given thread. Threads with a higher priority are executed in preference to threads with lower priorities. The invocation t.isDaemon()
checks whether the thread is a daemon. A daemon thread (which comes from UNIX terminology) is a low-priority thread that runs in the background to perform tasks such as garbage collection and so on. JVM does not wait for daemon threads before exiting whereas it waits for non-daemon threads.
The output of the program will look like this:
Name: main
ID: 1
Alive: true
Priority: 5
Daemon: false
New name: my-thread
The same code can be applied to any current thread, not just main.
Create custom threads
Java has two primary ways to create a new thread that performs a task you need.
- by extending the
Thread
class and overriding itsrun
method;
class HelloThread extends Thread {
@Override
public void run() {
String helloMsg = String.format("Hello, I'm %s", getName());
System.out.println(helloMsg);
}
}
- by implementing the
Runnable
interface and passing the implementation to the constructor of theThread
class.
class HelloRunnable implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
String helloMsg = String.format("Hello, I'm %s", threadName);
System.out.println(helloMsg);
}
}
In both cases, you should override the run
method, which is a regular Java method and contains code to perform a task. What approach to choose depends on the task and on your preferences. If you extend the Thread
class, you can accept fields and methods of the base class, but you cannot extend other classes since Java doesn't have multiple-inheritance of classes.
Here are two objects obtained by the approaches described above accordingly:
Thread t1 = new HelloThread(); // a subclass of Thread
Thread t2 = new Thread(new HelloRunnable()); // passing runnable
And here's another way to specify the name of your thread by passing it to the constructor:
Thread myThread = new Thread(new HelloRunnable(), "my-thread");
If you are already familiar with lambda expressions, you may implement the whole thing like this:
Thread t3 = new Thread(() -> {
System.out.println(String.format("Hello, I'm %s", Thread.currentThread().getName()));
});
Now you've created objects for threads, but you're not done yet. To perform the tasks you need, you'll have to start them.
Starting threads
The Thread
class has a method called start()
that is used to start a thread. At some point after you invoke this method, the method run
will be invoked automatically, but it'll not happen immediately.
Let's suppose that inside the main method you create a HelloThread
object named t
and start it.
Thread t = new HelloThread(); // an object representing a thread
t.start();
Eventually, it prints something like:
Hello, I'm Thread-0
Here's a picture that explains how a thread actually starts and why it is not happening immediately.
As you can see, there is some delay between starting a thread and the moment when it really starts working (running).
By default, a new thread is running in non-daemon mode. Reminder: the difference between daemon and non-daemon mode is that JVM will not terminate the running program while there are non-daemon threads left, whereas the daemon threads won't prevent the JVM from terminating.
Do not confuse the methods run
and start
. You must invoke start
if you'd like to execute your code inside run
in a separate thread. If you invoke run
directly, the code will be executed in the thread you call run
from.
If you try to start a thread more than once, the start
method throws IllegalThreadStateException
.
Despite the fact that within a single thread all statements are executed sequentially, it is impossible to determine the relative order of statements between multiple threads without additional measures; which we will not consider in this lesson.
Consider the following code:
public class StartingMultipleThreads {
public static void main(String[] args) {
Thread t1 = new HelloThread();
Thread t2 = new HelloThread();
t1.start();
t2.start();
System.out.println("Finished");
}
}
The order of displaying messages may be different. Here is one of them:
Hello, I'm Thread-1
Finished
Hello, I'm Thread-0
It is even possible that all threads print their text after the main thread prints "Finished":
Finished
Hello, I'm Thread-0
Hello, I'm Thread-1
This means that even though we call the start
method sequentially for each thread, we do not know when the run
method will actually be called.
Do not rely on the order of execution of statements between different threads, unless you've taken special measures.
A simple multithreaded program
Let's consider a simple multithreaded program with two threads. The first thread reads numbers from the standard input and prints out their squares. At the same time, the main thread occasionally prints messages to the standard output. Both threads work simultaneously.
Here is a thread that reads numbers in a loop and squares them. It has a break statement to stop the loop if the given number is 0.
class SquareWorkerThread extends Thread {
private final Scanner scanner = new Scanner(System.in);
public SquareWorkerThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
int number = scanner.nextInt();
if (number == 0) {
break;
}
System.out.println(number * number);
}
System.out.println(String.format("%s finished", getName()));
}
}
Inside the main
method, the program starts execution of an object of the SquareWorkerThread
class and writes messages to the standard output.
public class SimpleMultithreadedProgram {
public static void main(String[] args) {
Thread worker = new SquareWorkerThread("square-worker");
worker.start(); // start a worker (not run!)
for (long i = 0; i < 5_555_555_543L; i++) {
if (i % 1_000_000_000 == 0) {
System.out.println("Hello from the main thread!");
}
}
}
}
Here is an example of inputs and outputs with comments:
Hello from the main thread! // the program outputs it
2 // the program reads it
4 // the program outputs it
Hello from the main thread! // outputs it
3 // reads it
9 // outputs it
5 // reads it
Hello from the main thread! // outputs it
25 // outputs it
0 // reads it
square-worker finished // outputs it
Hello from the main thread! // outputs it
Hello from the main thread! // outputs it
Process finished with exit code 0
As you can see, this program performs two tasks "at the same time": one in the main thread and another one in the worker thread. It may not be "the same time" in the physical sense, however, both tasks are given some time to be executed.
Executors
We've already learned how to create threads by extending the Thread
class or implementing the Runnable
interface. Both ways allow you to create an object that represents a thread and start it to perform a piece of code in a separated thread. While it is easy to create several threads and start them, it becomes a problem when your application has hundreds or even thousands of threads running concurrently.
In addition, Thread
is a relatively low-level class and mixing it with the high-level code of your application may lead to unreadable code and poor architecture in the future. It may also produce some well-known errors such as invoking run()
instead of start()
.
Tasks and executors
To simplify the development of multi-threaded applications, Java provides an abstraction called ExecutorService
(or simply executor). It encapsulates one or more threads into a single pool and puts submitted tasks in an internal queue to execute them by using the threads.
This approach clearly isolates tasks from threads and allows you to focus on tasks. You do not need to worry about creating and managing threads because the executor does it for you.
Creating executors
All types of executors are located in the java.util.concurrent
package. You need to import it first. This package also contains a convenient utility class called Executors
for creating different types of ExecutorService
.
First of all, let's create an executor with exactly four threads in the pool:
ExecutorService executor = Executors.newFixedThreadPool(4);
It can execute multiple tasks concurrently and speed up your program by performing somewhat parallel computations. If one of the threads dies, the executor creates a new one. We will consider later how you can determine the required number of threads.
Submitting tasks
An executor has the submit
method that accepts a Runnable
task to be executed. Since Runnable
is a functional interface, it is possible to use a lambda expression as a task.
As an example, here we submit a task that prints "Hello!" to the standard output.
executor.submit(() -> System.out.println("Hello!"));
Of course, we can declare a class that implements Runnable
for our task, and then submit an object of this class. But it is very convenient to use lambda expressions together with executors for short tasks.
After invoking submit
, the current thread does not wait for the task to complete. It just adds the task to the executor's internal queue to be executed asynchronously by one of the threads.
The method also has several overloads which we will consider in upcoming topics.
Stopping executors
An executor continues to work after the completion of a task since threads in the pool are waiting for new coming tasks. Your program will never stop while at least one executor still works.
There are two methods for stopping executors:
void shutdown()
waits until all running tasks are completed and prohibits submitting of new tasks;List<Runnable> shutdownNow()
immediately stops all running tasks and returns a list of the tasks that were awaiting execution.
Note that shutdown()
does not block the current thread unlike join()
of Thread
. If you need to wait until the execution is complete, you can invoke awaitTermination(...)
with the specified waiting time.
ExecutorService executor = Executors.newFixedThreadPool(4);
// submitting tasks
executor.shutdown();
boolean terminated = executor.awaitTermination(60, TimeUnit.MILLISECONDS);
if (terminated) {
System.out.println("The executor was successfully stopped");
} else {
System.out.println("Timeout elapsed before termination");
}
An example: names of threads and tasks
In the following example, we create one executor with a pool consisting of four threads. We submit ten tasks to it and then analyze the results. Each task prints the name of a thread that executes it, as well as the name of the task.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
private final static int POOL_SIZE = 4;
private final static int NUMBER_OF_TASKS = 10;
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE);
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
int taskNumber = i;
executor.submit(() -> {
String taskName = "task-" + taskNumber;
String threadName = Thread.currentThread().getName();
System.out.printf("%s executes %s\n", threadName, taskName);
});
}
executor.shutdown();
}
}
If you launch this program many times, you will get a different output. Below is one of the possible outputs:
pool-1-thread-1 executes task-0
pool-1-thread-2 executes task-1
pool-1-thread-4 executes task-3
pool-1-thread-3 executes task-2
pool-1-thread-3 executes task-7
pool-1-thread-3 executes task-8
pool-1-thread-3 executes task-9
pool-1-thread-1 executes task-6
pool-1-thread-4 executes task-5
pool-1-thread-2 executes task-4
It clearly demonstrates the executor uses all four threads to solve the tasks. The number of solved tasks by each thread can vary. There are no guarantees what we'll get.
If you do not know how many threads are needed in your pool, you can take the number of available processors as the pool size.
int poolSize = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(poolSize);
Types of executors
We have considered the most used executor type which has a fixed pool size. Here are a few more types:
- An executor with a single thread
The simplest executor has only a single thread in the pool. It may be enough for async execution of rarely submitted and small tasks.
ExecutorService executor = Executors.newSingleThreadExecutor();
Important: one thread may not have time to process all incoming tasks, and the queue will grow significantly, consuming all the memory.
- An executor with a growing pool
There is also an executor that automatically increases the number of threads as needed and reuse previously constructed threads.
ExecutorService executor = Executors.newCachedThreadPool();
It can typically improve the performance of programs that perform many short-lived asynchronous tasks. But it can also lead to problems when the collection of threads increases too much. It is preferable to choose the fixed thread-pool executor whenever possible.
- An executor that schedules a task
If you need to perform the same task periodically or only once after the given delay, use the following executor:
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
The method scheduleAtFixedRate
submits a periodic Runnable
task that becomes enabled first after the given initDelay
, and subsequently with the given period
.
Here is a quick example with scheduling:
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() ->
System.out.println(LocalTime.now() + ": Hello!"), 1000, 1000, TimeUnit.MILLISECONDS);
Here is a fragment of the output:
02:30:06.375392: Hello!
02:30:07.375356: Hello!
02:30:08.375376: Hello!
...and even more...
It can be stopped as we did before.
This kind of executor also has a method named schedule
that starts a task only once after the given delay and another method scheduleWithFixedDelay
that starts the task with a fixed wait after the previous one is completed.
Exception handling
In our examples, we often ignore error handling to simplify code. Here we demonstrate one feature related to the handling of exceptions in executors (namely, unchecked).
What do you think the following code will print?
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println(2 / 0));
It does not print anything at all, including the exception! This is why it is common practice to wrap a task in the try-catch
block not to lose the exception.
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
try {
System.out.println(2 / 0);
} catch (Exception e) {
e.printStackTrace();
}
});
Now you will see the exception. In real applications, it is better to use some kind of logging to output it. Note that the executor will still work after the exception because it dynamically creates a new thread.