Concepts / Creating Threads with Runnable

Creating Threads with Runnable

ExecutorService is a Java interface in java.util.concurrent that defines services for executing submitted tasks, and it is used to manage thread pools.

  • Programming

Why Hand Work to a Pool

Suppose a program has many small pieces of work that can run in parallel. The direct approach is to create a new thread for each piece, start it, and let it finish. That works, but the program keeps making and discarding threads, and it has no built-in way to limit how many run at once.

Java provides a higher-level tool for this situation called ExecutorService. Instead of managing individual threads yourself, you hand work to an executor and let it manage a group of reusable threads, called a thread pool. A pool is a set of threads that exists ahead of time. Work is given to the pool, and the pool's threads pick that work up. The same threads are reused for many tasks rather than being created for each one.

This article follows one approach: start from running code, watch where a submitted task goes, and then name the pieces that made that path happen. Read the setup code first, then predict what each line changes before reading the explanation that follows.

Following One Submitted Task

Before looking at any code, trace a single task. You create a Runnable and call executor.execute(task). The important questions are: where does the task go, which thread runs it, and in what order do the steps happen? The diagram below shows the answer in sequence. Your main thread makes the call. The executor accepts the task, and the call returns to your main thread immediately. Later, a pool thread removes the task from the queue of waiting tasks and runs it.

Notice what is missing from this sequence: the main thread does not wait for step 4. The execute call returns after step 2, which means the task may not have started, or finished, by the time your next line runs. The task's work happens on a pool thread, so the path leads to the pool, not back to the submitting thread.

Getting an ExecutorService

Definition

ExecutorService is a Java interface in java.util.concurrent that defines services for executing submitted tasks, and it is used to manage thread pools.

A thread pool is created by calling Executors.newFixedThreadPool(n), where n is an int. This method creates a thread pool with n threads. The reason it is written this way is explained in the subsections below.

Where Submitted Tasks Wait

A task is submitted with executor.execute(task). The source says task is a Runnable object, and the method is used to submit it for execution. The important detail is what the call does and does not do. The method returns immediately after placing task into a queue of waiting tasks. It does not wait for the task to run.

After a task is in the queue, the threads in the pool take tasks off the queue and execute them. There are three stages for a submitted task: it is handed to execute, it waits in the queue, and then a pool thread removes it and runs it. The diagram below shows this path, with your code on one side, the queue between them, and the pool threads on the other side.

Read the diagram as a flow of work. Your code pushes tasks into the queue one at a time. The n pool threads pull tasks out of that same queue. When there are more submitted tasks than threads, the extra tasks simply remain in the queue until a pool thread becomes available to take them. The same threads keep returning to the queue, which is why they are reused across many tasks.

Tracing a Small Program

The program below is an original example that follows the pattern described in the source. It creates a pool sized to the processor count, creates five small Runnable tasks, and submits each one with execute. Read it closely and predict what each line changes before reading the walkthrough.

Code walkthrough
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PoolDemo {
    public static void main(String[] args) {
        int n = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(n);
        for (int i = 1; i <= 5; i++) {
            final int id = i;
            Runnable task = () -> System.out.println("Task " + id + " running");
            executor.execute(task);
        }
        System.out.println("All tasks submitted");
    }
}
  1. 1
    Line 1 to 2 import the ExecutorService interface and the Executors factory class.
  2. 2
    Line 6 asks the runtime how many processors are available and stores that count in n.
  3. 3
    Line 7 asks Executors for a fixed pool of n threads. The variable type is the interface, not a concrete class.
  4. 4
    Lines 8 to 12 build one Runnable per loop pass and submit each with execute. Each call places the task in the queue and returns.
  5. 5
    Line 13 prints after the loop. Because execute does not wait for tasks to run, this line can appear before some or all of the task messages.
  • n is the processor count reported by the machine
  • The pool is created with n threads
  • Each Runnable is submitted with execute, which returns immediately
  • The final line runs right after the loop, not after the tasks finish

The order and thread names in the output can vary between runs. What the program guarantees is the path: each task is handed to execute, waits in the queue, and is then taken by one of the pool's threads. The final message depends only on the loop finishing, not on the tasks running to completion.

Mistakes Beginners Make with Pools

The following mistakes come up when learners first combine Runnable tasks with a fixed pool. Each one is a mismatch between what the learner expects and where the path actually goes.

Finding Where an Expectation Broke

When a submitted task does not behave as expected, the useful question is where along the path the expectation broke. The source suggests checking three points in order, and each one maps to a stage you traced above.

  1. 1
    Check the pool size. Did the pool get the number of threads you intended, meaning the n passed to newFixedThreadPool? A mismatch here is a pool-size issue.
  2. 2
    Check the submission. Did the task reach the queue through execute? A mismatch here is a submission issue.
  3. 3
    Check the execution. Did a pool thread pick the task up and run it? A mismatch here is an execution issue.
Worked example

Tracing a Surprising Task Order

A learner expects the first submitted task to finish before the second task starts. The output shows the tasks interleaved in a different order. Where does the learner look first?

  1. 1
    Check the pool size

    The learner confirms the pool size is the intended n. If the pool has more than one thread, two tasks can run at overlapping times, so overlap alone is not a bug.

  2. 2
    Check the submission

    The learner confirms both tasks were passed to execute in the intended order. Submission order is not the same as completion order, because the source says pool threads take tasks from the queue and run them.

  3. 3
    Check the execution

    The learner notes which pool thread took each task. Pool threads are separate from the submitting thread, so the interleaving is an execution fact to explain, not a submission error.

Result

The learner locates the mismatch at the execution stage, where the expectation that submission order equals run order was wrong. The fix is in the mental model, not in the submission code.

Practice Tracing a Submission

MEDIUM

A program creates a fixed pool with n set to the processor count, then submits 12 Runnable tasks in a loop with execute. Immediately after the loop, it prints "Finished". A learner expects all 12 task messages to print before "Finished". Using the three-checkpoint method, identify which stage you would check first and what you would look for.

Hints
  • Start with the stage the source calls the submission: what does execute do when it returns?
  • Recall whether the source says the call waits for the task to run.
  • Then ask which stage would produce a mismatch between expected and actual output order.

Key Takeaways

  • ExecutorService is a Java interface in java.util.concurrent for executing submitted tasks and managing thread pools; you obtain one from the Executors class rather than creating it directly.
  • Executors.newFixedThreadPool(n) creates a pool of n reusable threads, and the source's example sets n to Runtime.getRuntime().availableProcessors().
  • executor.execute(task) places a Runnable in a queue of waiting tasks and returns immediately; a pool thread later removes the task from the queue and runs it.
  • When a task misbehaves, check in order: the pool size (n), the submission through execute, and the execution on a pool thread.