Thread pools in Java concurrency

Before writing production code based on Java's executors, we should learn another inseparable concept of concurrency, thread pools.

This article can be considered as a continuation of the article about executor service in Java. Its first part will explain what thread pools are. The second part will present thread pools in Java. After that, we'll see thread pools in action.

What thread pool are ?

Thread pool pattern looks like object pool design pattern. We create an container (pool) which holds a specific number of objects (threads in occurrence). The specificity of thread pool is that the threads created inside the pool must perform some tasks which are usually placed in a queue. When all tasks are executed, thread can both terminate or waiting for another tasks.

A big advantage of thread pool over threads creation at demand, is the memory influence. New thread creation and destruction for every task are the operations using a lot of memory which can be overloaded quickly. Thread recycling through a pool doesn't have so bad effects on application performance. In additionally, by using thread pool we don't need to deal with opening or closing threads because it's handled internally by Java's concurrent package. It limits a source of application bugs.

Thread pools in Java

Tasks executed in thread pool as called worker threads. As we mentioned earlier, threads are recycled. It's why worker threads can support only Runnable or Callable instances. Different types of thread pools are supported by Java's concurrent package:

Java 8 introduces a supplementary type of thread pool, called work-stealing pool. Unlike previously seen pools, it determines the threads quantity automatically. Instead of taking this quantity in parameter, work stealing pool calculates it thanks to parallelism level specified in factory method newWorkStealingPool(int parallelism).

Thread pool can be got through one of Executors factory method: newFixedThreadPool for fixed-size, newCachedThreadPool for cached, newSingleThreadExecutor for single and newWorkStealingPool for stealing-work pool. Another way to create the pools are following classes: ScheduledThreadPoolExecutor and ThreadPoolExecutor.

Thread pools example in Java

In our test case we'll use thread sleeping feature to synchronize threads execution and check if expected number of threads is executed.

public class ThreadPoolTest {
        
  @Test
  public void testFixed() throws InterruptedException {
    ExecutorService fixedPool = Executors.newFixedThreadPool(2);
    fixedPool.execute(new SampleTask("Task-1", Executions.fixed));
    fixedPool.execute(new SampleTask("Task-2", Executions.fixed));
    fixedPool.execute(new SampleTask("Task-3", Executions.fixed));
    fixedPool.execute(new SampleTask("Task-4", Executions.fixed));
    
    Thread.sleep(50);
    
    assertTrue("2 tasks should be running, but only "+ Executions.fixed.getValue() + " were", 
      Executions.fixed.getValue() == 2);
    System.out.println("----------------- executing new threads --------------------------");
    Executions.fixed.reset();
    
    Thread.sleep(4000);

    assertTrue("2 tasks should be running, but only "+ Executions.fixed.getValue() + " were", 
      Executions.fixed.getValue() == 2);
  }
        
  @Test
  public void testCached() throws InterruptedException {
    ExecutorService cachedPool = Executors.newCachedThreadPool();
    cachedPool.execute(new SampleTask("Task-1", Executions.cached));
    cachedPool.execute(new SampleTask("Task-2", Executions.cached));

    Thread.sleep(2000);

    assertTrue("2 tasks should be running, but only "+ Executions.cached.getValue() + " were", 
      Executions.cached.getValue() == 2);
    System.out.println("----------------- executing new threads --------------------------");
    Executions.cached.reset();
    cachedPool.execute(new SampleTask("Task-3", Executions.cached));
    cachedPool.execute(new SampleTask("Task-4", Executions.cached));
    cachedPool.execute(new SampleTask("Task-5", Executions.cached));
    cachedPool.execute(new SampleTask("Task-6", Executions.cached));
    cachedPool.execute(new SampleTask("Task-7", Executions.cached));

    Thread.sleep(3000);
    
    assertTrue("5 tasks should be running, but only "+ Executions.cached.getValue() + " were", 
      Executions.cached.getValue() == 5);
    System.out.println("----------------- executing new threads --------------------------");
    Executions.cached.reset();
    cachedPool.execute(new SampleTask("Task-8", Executions.cached));
    cachedPool.execute(new SampleTask("Task-9", Executions.cached));
    cachedPool.execute(new SampleTask("Task-10", Executions.cached));
    cachedPool.execute(new SampleTask("Task-11", Executions.cached));
    cachedPool.execute(new SampleTask("Task-12", Executions.cached));
    cachedPool.execute(new SampleTask("Task-13", Executions.cached));
    cachedPool.execute(new SampleTask("Task-14", Executions.cached));
    cachedPool.execute(new SampleTask("Task-15", Executions.cached));
    
    Thread.sleep(3000);

    assertTrue("8 tasks should be running, but only "+ Executions.cached.getValue() + " were", 
      Executions.cached.getValue() == 8);
  }
        
  @Test
  public void testSingle() throws InterruptedException {
    ExecutorService fixedPool = Executors.newSingleThreadExecutor();
    fixedPool.execute(new SampleTask("Task-1", Executions.single));
    fixedPool.execute(new SampleTask("Task-2", Executions.single));
    fixedPool.execute(new SampleTask("Task-3", Executions.single));
    fixedPool.execute(new SampleTask("Task-4", Executions.single));
    
    Thread.sleep(50);

    assertTrue("1 task should be running, but only "+ Executions.single.getValue() + " were", 
      Executions.single.getValue() == 1);
    Executions.single.reset();
    for (int i = 0; i < 3; i++) {
      System.out.println("----------------- executing new thread --------------------------");

      Thread.sleep(1000);

      assertTrue("1 task should be running, but only "+ Executions.single.getValue() + " were", 
        Executions.single.getValue() == 1);
      Executions.single.reset();
    }
  }

}

class SampleTask implements Runnable {

  private String name;
  private Executions executions;
  
  public SampleTask(String name, Executions exe) {
    this.name = name;
    executions = exe;
  }
  
  public SampleTask(String name) {
    this.name = name;
  }
  
  @Override
  public void run() {
    this.executions.increment();
    System.out.println(this.name+": executing SampleTask");
    Thread.sleep(1000);
    System.out.println(this.name+": was executed");
  }
}


enum Executions {
  fixed, cached, single;
  
  private int value = 0;
  
  public void reset() {
    this.value = 0;
  }
  
  public synchronized void increment() {
    this.value++;
  }
  
  public int getValue() {
    return value;
  }
}

The test should pass well. There are no mysterious things to explain. As we expected, in fixed-size thread pool, a fixed number of thread is created. In cached pool, the pool's capacity increases at demand. Regarding to single thread pool, only one thread is executed at given moment.

Pooling, known usually from object pool design pattern, was implemented in Java concurrency environment. Thanks to fixed-sized, cached or single thread pools, we can better control the consequences of concurrent tasks on application. As we can see in this article, every pool type behaves differently when tasks number to treat grows. Fixed-sized pool puts all supplementary tasks in a queue and treats them when pool's threads are available. Cached pool makes new threads instead. Single-thread pool is working with only thread at a moment and the rest are placed in the queue.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!