Java Concurrency 2 - Executor Framework
date
Feb 19, 2022
slug
executor-framework
status
Published
tags
java
concurrency
coding
programming
summary
Java 5 provided the Executor framework and the idea of thread pools as a higher-level idea capturing the power of threads, which allows Java programmers to decouple task submission from task execution.
type
Post
Java 5 provided the Executor framework and the idea of thread pools as a higher-level idea capturing the power of threads, which allows Java programmers to decouple task submission from task execution.
The Java ExecutorService provides an interface where you can submit tasks and obtain their results later. The expected implementation uses a pool of threads, which can be created by one of the factory methods, such as the newFixedThreadPool method:
ExecutorService newFixedThreadPool(int nThreads);
This method creates an ExecutorService containing nThreads (often called worker threads) and stores them in a thread pool, from which unused threads are taken to run submitted tasks on a first-come, first-served basis.
The programmer provides a task (a Runnable or a Callable), which is executed by a thread.
Thread Pools And Why They’re Worse
Thread pools are better than explicit thread manipulation in almost all ways, but you need to be aware of two “gotchas”;
- A thread pool with k threads can execute only k tasks concurrently. Any further task submissions are held in a queue and not allocated a thread until one of the existing tasks completes. This situation is generally good, in that it allows you to submit many tasks without accidentally creating an excessive number of threads, but you have to be wary of tasks that sleep or wait for I/O or network connections. In the context of blocking I/O, these tasks occupy worker threads but do no useful work while they’re waiting. It’s even possible to cause deadlock in a thread pool if earlier task submissions or already running tasks, need to wait for later task submissions, which is a typical use-pattern for Futures.
- Java typically waits for all threads to complete before allowing a return from main to avoid killing a thread executing vital code. Therefore, it’s important in practice and as part of good hygiene to shut down every thread pool before exiting the program (because worker threads for this pool will have been created but not terminated, as they’re waiting for another task submission).
Java threads can be labeled as daemon or nondaemon, using the setDaemon() method call. Daemon threads are killed on exit (and therefore are useful for services that don’t leave the disk in an inconsistent state), whereas returning from main continues to wait for all threads that aren’t daemons to terminate before exiting the program.