Thread pool in Java

Creating a thread is time consuming and resource intensive. The number of threads that can be launched within one process is also limited. To avoid these problems and, in general, to manage multiple threads more efficiently in Java, a thread pool mechanism was implemented, which is created during application startup and then threads for processing requests are taken and reused from it. Thus, it becomes possible not to lose threads, to balance the application by the number of threads and the frequency of their creation.

Since Java 1.5, the Java API provides the Executor framework that allows you to create different types of thread pool:

  • Executor is a simplified pool interface that contains one method for submitting a task for execution;
  • ExecutorService - extended pool interface, with the ability to terminate all threads;
  • AbstractExecutorService - a pool base class that implements the ExecutorService interface;
  • Executors - a factory of objects associated with a thread pool, including the creation of basic types of pools;
  • ThreadPoolExecutor - flexible thread pool, can serve as a base class for non-standard pools;
  • ForkJoinPool - a pool for performing tasks like ForkJoinTask.

Executors methods for creating pools:

  • newCachedThreadPool() - if there is a free thread, then the task is executed in it, otherwise a new thread is added to the pool. Threads not used for more than a minute are terminated and the cache is also deleted. The pool size is unlimited. Designed to perform many small asynchronous tasks.
  • newCachedThreadPool(ThreadFactory threadFactory) - similar to the previous one, but with its own thread factory
  • newFixedThreadPool(int nThreads) - creates a pool for the specified number of threads. If new tasks are added when all threads are active, then they will be stored in the queue for later execution. If one of the threads terminated due to an error, another thread will be started in its place. Threads live until the pool is closed explicitly by the shutdown() method.
  • newFixedThreadPool(int nThreads, ThreadFactory threadFactory) - similar to the previous one, but with its own thread factory
  • newSingleThreadScheduledExecutor() is a single-threaded pool with the ability to execute a task after a specified time or periodically. If the thread was terminated due to any errors, then a new thread will be created to perform the next task.
  • newSingleThreadScheduledExecutor(ThreadFactory threadFactory) - similar to the previous one, but with its own thread factory
  • newScheduledThreadPool(int corePoolSize) - a pool for performing tasks after a specified time or periodically
  • newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) - similar to the previous one, but with its own thread factory
  • unconfigurableExecutorService(ExecutorService executor) - a wrapper for the pool that prohibits changing its configuration

How big should the thread pool be?

When adjusting the size of the thread pool, it is important to avoid two pitfalls: too few threads (the execution queue will grow, consuming a lot of memory) or too many threads (slowing down the entire system due to frequent context switches).

The optimal thread pool size depends on the number of available processors and the nature of the tasks in the work queue. On an N-processor system, for a work queue that will only run tasks with a limited compute rate, you can achieve maximum CPU utilization with a thread pool that contains N or N + 1 threads. Tasks that might be waiting for I/O (input/output) to take place — for example, a task that reads an HTTP request from a socket — might need to increase the pool size beyond the number of available processors because not all threads will run all the time. Using profiling, you can estimate the ratio of wait time (WT) to processing time (ST) for a typical request. If we call this ratio WT/ST, then for an N-processor system, approximately N * (1 + WT/ST) threads will be needed to fully load the processors.

CPU usage is not the only factor that matters when adjusting the size of the thread pool. As the thread pool grows, you might run into constraints on the scheduler, available memory, or other system resources such as the number of sockets, open file descriptors, or database communication channels.

What happens if the thread pool queue is already full, but a new task is submitted?

If the thread pool queue is full, then the submitted task will be "rejected". For example, the submit() method of the ThreadPoolExecutor throws a RejectedExecutionException, after which the RejectedExecutionHandler is called.

What is the difference between submit() and execute() on a thread pool?

Both methods are ways of submitting a task to a thread pool, but there is a slight difference between them.

execute(Runnable command) is defined in the Executor interface and executes the submitted task and returns nothing.

submit() is an overloaded method defined in the ExecutorService interface. Able to accept tasks of Runnable and Callable types and return a Future object, which can be used to control and manage the execution process, and obtain its result.


Read also:


Comments

Popular posts from this blog

Methods for reading XML in Java

XML, well-formed XML and valid XML

ArrayList and LinkedList in Java, memory usage and speed