Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Executors and Thread Pool | High-level Synchronization Mechanisms
Multithreading in Java
course content

Course Content

Multithreading in Java

Multithreading in Java

1. Multithreading Basics
2. Synchronized Collections
3. High-level Synchronization Mechanisms
4. Multithreading Best Practices

bookExecutors and Thread Pool

We've already explored a variety of mechanisms for supporting multithreading, and Executors is one of them!

What are Executors and Thread Pooling?

Executors is a mechanism that offers high-level abstractions for handling threads. It enables you to create and manage a thread pool, which consists of a set of pre-existing threads that are ready to execute tasks. Instead of creating a new thread for every task, tasks are sent to the pool, where their execution is distributed among the threads.

So, what exactly is a thread pool? It is a collection of pre-existing threads that are ready to execute tasks. By using a thread pool, you avoid the overhead of creating and destroying threads repeatedly, as the same threads can be reused for multiple tasks.

Note

If there are more tasks than threads, the tasks wait in the Task Queue. A task from the queue is handled by an available thread from the pool, and once the task is completed, the thread picks up a new task from the queue. Once all tasks in the queue are finished, the threads stay active and wait for new tasks.

Example From Life

Think of a restaurant where cooks (threads) prepare orders (tasks). Instead of hiring a new cook for each order, the restaurant employs a limited number of cooks who handle orders as they come in. Once one cook finishes an order, they take on the next one, which helps to make efficient use of the restaurant's resources.

Main Method

newFixedThreadPool(int n): Creates a pool with a fixed number of threads equal to n.

java

Main

copy
1
ExecutorService executorService = Executors.newFixedThreadPool(20);

newCachedThreadPool(): Creates a pool that can create new threads as needed, but will reuse available threads if there are any.

java

Main

copy
1
ExecutorService executorService = Executors.newCachedThreadPool();

newSingleThreadExecutor(): Creates a single thread pool that ensures that tasks are executed sequentially, that is, one after the other. This is useful for tasks that must be executed in strict order.

java

Main

copy
1
ExecutorService executorService = Executors.newSingleThreadExecutor();

In all the examples, the methods of Executors return an implementation of the ExecutorService interface, which is used to manage threads.

ExecutorService provides methods for managing a pool of threads. For instance, submit(Runnable task) accepts a task as a Runnable object and places it in a queue for execution. It returns a Future object, which can be used to check the status of the task and obtain a result if the task produces a result.

java

Main

copy
12345678910111213141516171819202122232425262728293031323334
package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }

The method shutdown() starts a graceful shutdown of the thread pool. It stops accepting new tasks but will complete the current tasks. Once you call this method, the pool cannot be restarted.

The method awaitTermination(long timeout, TimeUnit unit) waits for all tasks in the pool to finish within the given time frame. This is a blocking wait that allows you to ensure all tasks are completed before finalizing the pool.

Also we didn't mention the main interface that helps to track the state of the thread, it's the Future interface. The submit() method of the ExecutorService interface returns an implementation of the Future interface.

If you want to get the result of thread execution, you can use get() method, if the thread implements Runnable, get() method returns nothing, but if Callable<T>, it returns T type.

java

Main

copy
12345678910111213141516171819202122232425262728293031
package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }

You can also use the cancel(boolean mayInterruptIfRunning) method to attempt to cancel the execution of a task. If the task has not started yet, it will be canceled. If the task is already running, it may be interrupted based on the mayInterruptIfRunning flag.

true: If the task is running, it will be interrupted by calling Thread.interrupt() on the executing thread. false: If the task is running, it will not be interrupted, and the cancellation attempt will have no effect on the currently running task.

Well and 2 methods that intuitively understand what they do:

  • isCancelled(): Checks if the task has been canceled;
  • isDone(): Checks if the task has been completed.

Example of Use

It is maximally efficient to use the number of threads = processor cores. You can see this in the code using Runtime.getRuntime().availableProcessors().

java

Main

copy
1
int availableProcessors = Runtime.getRuntime().availableProcessors();

Differences between Creating Threads Directly and using ExecutorService

The main differences between creating threads directly and using ExecutorService are convenience and resource management. Manual creation of threads requires managing each thread individually, which complicates the code and administration.

ExecutorService streamlines management by using a thread pool, making task handling easier. Additionally, while manual thread creation can lead to high resource consumption, ExecutorService allows you to customize the size of the thread pool.

Everything was clear?

How can we improve it?

Thanks for your feedback!

Section 3. Chapter 6
We're sorry to hear that something went wrong. What happened?
some-alt