Configuring Thread Pools

Your Own Executor

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Bean(name = "notificationExecutor")
    public ThreadPoolTaskExecutor notificationExecutor() {
        ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
        ex.setCorePoolSize(4);
        ex.setMaxPoolSize(16);
        ex.setQueueCapacity(500);
        ex.setThreadNamePrefix("notify-");
        ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        ex.setWaitForTasksToCompleteOnShutdown(true);
        ex.setAwaitTerminationSeconds(30);
        ex.initialize();
        return ex;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            LoggerFactory.getLogger("async").error("Async {} failed: {}", method.getName(), ex.getMessage());
    }
}

// pick the pool by bean name
@Async("notificationExecutor")
public void sendOrderEmail(String email, Long orderId) { ... }

Or Just Properties

Example

Spring Boot's default executor can be tuned without code:

application.yml

spring:
  task:
    execution:
      thread-name-prefix: shopease-async-
      pool:
        core-size: 8
        max-size: 32
        queue-capacity: 1000
      shutdown:
        await-termination: true
        await-termination-period: 30s

How a Pool Grows

A ThreadPoolExecutor fills core threads first, then the queue, and only then creates extra threads up to max. When everything is full the rejection policy runs.

Example

Watch a tiny pool (core 1, queue 2, max 2) receive 5 long-running tasks with the default AbortPolicy:

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        CountDownLatch release = new CountDownLatch(1);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());

        for (int i = 1; i <= 5; i++) {
            int task = i;
            try {
                pool.execute(() -> { try { release.await(); } catch (InterruptedException e) { } });
                System.out.printf("task %d accepted -> threads=%d, queued=%d%n",
                    task, pool.getPoolSize(), pool.getQueue().size());
            } catch (RejectedExecutionException e) {
                System.out.println("task " + task + " REJECTED (threads and queue are full)");
            }
        }
        release.countDown();
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("completed tasks: " + pool.getCompletedTaskCount());
    }
}

Output

task 1 accepted -> threads=1, queued=0
task 2 accepted -> threads=1, queued=1
task 3 accepted -> threads=1, queued=2
task 4 accepted -> threads=2, queued=2
task 5 REJECTED (threads and queue are full)
completed tasks: 4

Tip: CallerRunsPolicy is a safer choice for web apps: instead of failing, the request thread runs the task itself, which naturally slows down new work.