Basics of Multithreading in Java
In this article we’re going to talk about multithreading in Java. Multithreading is a programming technique where multiple threads run concurrently within a single program, enabling tasks to be performed in parallel.
Multithreading can help improve performance and responsiveness, especially in scenarios like:
- Handling multiple requests
- Spring boot for example uses multiple threads to handle multiple requests concurrently
- Performing background operations
- In apps blocking IO requests un on a separate thread to the main UI thread
- Utilizing multicore processors efficiently
- When performing a computationally tasks can be spread across threads
Creating a thread by extending Thread
A thread is the smallest unit of a program that can execute independently. Java provides built-in support for multithreading through the Thread class. To create a thread that will perform a certain task extend the Thread class and override it’s run() method with the operation you want to perform.
public class PrinterThread extends Thread {
private final String name;
public PrinterThread(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Started " + name);
System.out.println("Finished " + name);
}
}
To start a Thread we simply instantiate one and call the start() method. This will start the thread running and it’ll continue until it has completed or it is interrupted using the interrupt() method.
public class MainThread {
public static void main(String[] args) {
Thread thread = new PrinterThread("Task");
thread.start();
}
}
This would print the following to the console.
Started Task
Finished Task
Separate out task by implementing a Runnable
Java has an interface called Runnable that represents a task or a unit of work. Instead of extending a Thread you can implement a Runnable that defines the task to perform and pass that to a Thread to actually execute the task. This decouples the task from the thread running the task.
public class PrinterRunnable implements Runnable {
private final String name;
public PrinterRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Started " + name);
System.out.println("Finished " + name);
}
}
To execute the task defined in the Runnables run() method you need to run it using a Thread by passing an instance to the Threads constructor. The Runnable class is a FunctionalInterface so you can provide a lambda instead of an instance of a Runnable.
public class MainRunnable {
public static void main(String[] args) {
Thread thread = new Thread(new PrinterRunnable("Runnable Task"));
// or using a functional interface
// Thread thread = new Thread(() -> System.out.println("Completed Runnable Task"));
thread.start();
}
}
Waiting for a thread to finish
Occasionally, you need to orchestrate when a task starts for example waiting until another task has completed before starting a new one. This can be done using the join() method which blocks until the thread has finish.
public class MainMultipleThreads {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new PrinterThread("Task 1");
Thread thread2 = new PrinterThread("Task 2");
Thread thread3 = new PrinterThread("Task 3");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
thread3.start();
}
}
This would print the following to the console. Notice Task 1 and 2 finished before Task 3 starts.
Started Task 1
Started Task 2
Finished Task 1
Finished Task 2
Started Task 3
Finished Task 3
Executors and thread pools
Java provides the Executors Framework for better management of threads. Instead of creating threads manually you can create an ExecutorService which accepts tasks and executes them using one or more threads. The executors reuse threads with can significantly improve performance as create new threads is expensive.
Here are some of the must useful executors:
SingleThreadExecutor: thread pool of a single thread that is reused.FixedThreadPool: thread pool of n threads that are reused.CachedThreadPool: thread pool that creates new thread as needed and reused.ScheduledExecutor: used to run task on a regular basis.
These can all be created using the builder methods on Executors e.g. newSingleThreadExecutor(), newFixedThreadPool(...), etc.
public class MainExecutor {
public static void main(String[] args) {
executorService = Executors.newFixedThreadPool(3);
Runnable task = () -> System.out.println("Running task " + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
executorService.submit(task);
}
executorService.shutdown();
}
}
This would print the following to the console.
Running task pool-1-thread-1
Running task pool-1-thread-2
Running task pool-1-thread-3
Running task pool-1-thread-1
...