Зміст курсу
Multithreading in Java
Multithreading in Java
Executors 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.
Main
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.
Main
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.
Main
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.
Main
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.
Main
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()
.
Main
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.
Дякуємо за ваш відгук!