Master Spring Boot Async: Build a Custom ThreadPool for Precise Concurrency Control
This tutorial shows how to replace the default Spring Boot @Async executor with a custom ThreadPoolTaskExecutor, explains each pool parameter, demonstrates annotating async methods to use the pool, and provides a unit test that verifies the thread naming and execution behavior.
Spring Boot’s @Async annotation enables asynchronous method execution, but controlling the number of concurrent tasks requires a custom thread pool.
Define a custom ThreadPoolTaskExecutor
Create a configuration class that enables async processing and declares a bean named taskExecutor:
@EnableAsync
@Configuration
public class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // threads created at startup
executor.setMaxPoolSize(20); // additional threads when the queue is full
executor.setQueueCapacity(200); // pending task buffer
executor.setKeepAliveSeconds(60); // idle thread timeout
executor.setThreadNamePrefix("taskExecutor-"); // for easier log tracing
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}Key pool parameters explained
Core pool size 10 – number of threads created when the application starts.
Maximum pool size 20 – extra threads are created only after the queue reaches its capacity.
Queue capacity 200 – buffer for tasks waiting to be executed.
Keep‑alive 60 s – idle threads beyond the core size are terminated after this period.
Thread name prefix – makes log output easier to trace (e.g., taskExecutor-1).
Rejected‑execution policy – CallerRunsPolicy runs the rejected task in the calling thread when the pool is saturated.
Use the custom pool in async methods
Annotate each asynchronous method with the bean name to force execution in the defined pool:
@Slf4j
@Component
public class Task {
private static final Random random = new Random();
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("Start task one");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("Task one finished, cost: {} ms", (end - start));
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("Start task two");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("Task two finished, cost: {} ms", (end - start));
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("Start task three");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("Task three finished, cost: {} ms", (end - start));
}
}Unit test to verify the configuration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
// Wait for async tasks to finish
Thread.currentThread().join();
}
}Verification
Running the test prints log lines whose thread names start with taskExecutor-, confirming that the asynchronous methods are executed by the custom thread pool.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
