Threads and Runnable Objects
A thread pool is a group of threads that can execute multiple tasks, with each task assigned to any idle thread in the pool rather than to a newly created thread.
Why One Thread per Job Falls Short
Imagine a program that must perform hundreds of small computations, such as processing hundreds of pieces of an image. A tempting design is to start a brand-new thread for every piece of work, let each thread do its one job, and then let it finish. The design is easy to picture, but it has a practical cost. Creating a thread carries a significant amount of overhead, and paying that cost again and again for a large number of small jobs wastes effort.
What do you think happens?
A program has hundreds of small image pieces to process. Should it start one new thread for each piece, or keep a small group of threads and feed them the pieces?
Reveal answer
Keep a small group of threads and feed them the pieces.
Creating a thread carries significant overhead, so repeating that cost for every small job wastes effort. A thread pool keeps a modest group of threads on hand and reuses them across many tasks.
The thread pool is built around the opposite idea of one thread per job. Instead, a program keeps a modest group of threads and sends jobs to them. To make that possible, each job first has to exist as something the program can pass around, and that is where the Runnable object comes in.
Packaging a Subtask as a Runnable
Before a job can be handed to a thread, it must be an object. In an object-oriented design, each subtask is usually represented by an object. Because a subtask is a computation, its object carries an instance method that performs that computation. Running the subtask then reduces to one action: calling that method. In the source's own program the method is named run(), and the task object implements the standard Runnable interface.
The point of the Runnable interface is that it provides a common, standard way to describe a computational task. Any code that knows how to run a Runnable can therefore run any task that follows the same pattern. The task does not need to know which thread will execute it, and the thread does not need to know what the task computes. It only needs to call run().
(no output until a thread calls run() on a TileTask object)- implements Runnable declares that this class follows the standard task pattern
- the instance field holds the data this particular subtask needs
run()holds the computation; nothing runs until some thread calls it
Separating the Work from the Worker
This separation is what makes the rest of the design possible. Once a task is an object with a run() method, the program does not need to know in advance which thread will eventually execute it. It only needs a way to get the object to some thread that is free to call run(). Task decomposition, the step of splitting a large job into independent subtasks, is the first half of this story. Packaging each subtask as a Runnable is the second half.
The diagram separates the three roles. The Runnable holds the code. The Thread is the worker that carries out the execution. The pool is the group that holds several such workers. A single Runnable can be run by whichever pooled thread is available, so the code and the execution are not permanently tied together.
Tracing a Task Through the Pool
When several threads are available for performing tasks, that group is called a thread pool. A new thread does not have to be created to handle each task. Instead, when a task needs to be performed, it is assigned to any idle thread in the pool. The flow below follows one task from submission to completion.
Notice that the task is not tied to a particular thread. Each task joins the pool, and whichever pooled thread happens to be idle takes it. A thread that finishes its work does not disappear. It becomes idle again and is ready for the next arriving task.
Thread Reuse Across Many Tasks
Because the pool is reused, the same few threads can carry out a long series of tasks over the life of the program. The contrast with the one-thread-per-job design is easiest to see side by side. Suppose three tiles of an image must be processed, labeled A, B, and C, and the pool has two threads.
Following Three Tiles Through a Two-Thread Pool
Three Runnable tasks, A, B, and C, are submitted to a pool of two threads. Trace which thread runs which task, and how many threads are created in the process.
- 1Submit Task A
Task A joins the pool and is assigned to an idle pooled thread. Pooled Thread 1 picks it up and calls
run(). - 2Submit Task B
Task B is assigned to the other idle pooled thread, Pooled Thread 2, which calls
run(). - 3Submit Task C
Task C is assigned to whichever pooled thread is idle. Pooled Thread 1 finished Task A and became idle again, so it takes Task C.
- 4Count threads
Only two threads exist in total. The one-thread-per-job design would have created three.
Pooled Thread 1 runs A then C, Pooled Thread 2 runs B, and two threads serve three tasks. This is an illustrative trace built from the pool behavior the source describes, not a timing claim.
Choosing the Size of the Pool
A pool needs a size, and no universal rule fixes it. The source is direct about this: the optimal number of threads is not entirely clear and depends on the particular problem being solved. The guiding goal, however, is clear. The aim is to keep all of the computer's processors busy. When every processor always has a thread ready to run, the hardware is being used well.
Mistakes Learners Make with Pools
Common mistake Believing that creating a Runnable starts a thread
What it looks likeA program builds a TileTask object and expects its computation to already be running.
Why it failsA Runnable is an object that describes work. Nothing executes until a thread calls its
run()method.Do this insteadThink of the Runnable as a description of the job, and the thread as the worker that performs it.
Common mistake Assuming each task must get its own thread
What it looks likeA program with hundreds of small tiles creates hundreds of threads, one per tile.
Why it failsCreating a thread carries significant overhead, and the pool exists so that a new thread does not have to be created for each task.
Do this insteadKeep a modest group of threads and let each one execute several tasks in turn.
Common mistake Tying a task to a specific thread
What it looks likeA program expects Task C to run on the same thread that ran Task A.
Why it failsTasks are not tied to particular threads. Any idle pooled thread can take a task.
Do this insteadLet the pool assign work to whichever thread is idle.
Common mistake Using one thread per processor for every problem
What it looks likeA program whose tasks mostly wait on data gets one thread per processor and leaves processors idle while threads are blocked.
Why it failsA blocked thread leaves its processor with nothing to do unless another thread is ready to run.
Do this insteadFor blocking work, allow extra threads so that other threads can run while some are waiting.
Sizing a Pool for Two Workloads
A program has eight processors. Workload 1 processes image tiles, and each tile is almost pure computation. Workload 2 downloads files, and each download spends most of its time waiting for data over a connection. Which workload suggests a pool size near eight threads, and why would the other workload benefit from more threads than processors?
Hints
- Ask whether a thread on that workload is usually running or usually blocked.
- A blocked thread leaves its processor idle unless another thread is ready to run.
Key Takeaways
- A thread pool is a group of threads that executes multiple tasks, with each task assigned to any idle thread in the pool rather than to a newly created thread.
- Each subtask is usually packaged as a Runnable object whose
run()method performs the computation, so any available thread can execute it. - The Runnable holds the code, the thread performs the execution, and the pool reuses a few threads across many tasks to avoid the overhead of creating a thread for each one.
- The optimal pool size depends on the problem: one thread per processor suits computation-bound work, while tasks that block while waiting benefit from extra threads.