DEV Community

realNameHidden
realNameHidden

Posted on

Java Thread Pools Explained with End-to-End Examples (Fixed, Cached, Single, Scheduled)

Introduction

Imagine you’re running a delivery service.

  • Some deliveries are predictable and steady
  • Some are sudden and short-lived
  • Some must be handled one at a time
  • Some must run every day at 9 AM

Would you hire the same type of workers for all of these tasks? Of course not.

👉 Java thread pools work exactly the same way.

Each thread pool type is designed for a specific workload pattern. Using the wrong one can lead to:

  • Performance issues
  • Resource exhaustion
  • Unpredictable behavior in production

In this blog, you’ll learn:

  • All major Java thread pool types
  • When to use each one
  • A single Spring Boot app demonstrating all of them
  • End-to-end REST APIs, with curl requests and responses

Core Concepts: Thread Pools in Simple Terms

A thread pool is a reusable group of threads managed by Java’s ExecutorService.

Instead of creating threads manually:

  • Tasks are submitted
  • Threads pick tasks from a queue
  • Threads are reused

This gives you:
✔ Better performance
✔ Controlled concurrency
✔ Safer multithreading


Thread Pool Types You Must Know

Thread Pool When to Use
FixedThreadPool Stable number of tasks
CachedThreadPool Short-lived async tasks
SingleThreadExecutor Sequential execution
ScheduledThreadPool Delayed or periodic tasks

Let’s build one Spring Boot app that demonstrates all four.


End-to-End Setup (Spring Boot + Java 21)

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Thread Pool Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@Configuration
public class ThreadPoolConfig {

    @Bean
    public ExecutorService fixedThreadPool() {
        return Executors.newFixedThreadPool(3);
    }

    @Bean
    public ExecutorService cachedThreadPool() {
        return Executors.newCachedThreadPool();
    }

    @Bean
    public ExecutorService singleThreadExecutor() {
        return Executors.newSingleThreadExecutor();
    }

    @Bean
    public ScheduledExecutorService scheduledThreadPool() {
        return Executors.newScheduledThreadPool(2);
    }
}
Enter fullscreen mode Exit fullscreen mode

1️⃣ FixedThreadPool – Stable Workload

When to Use

  • Known number of concurrent tasks
  • Predictable traffic
  • Backend processing

REST API Example

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutorService;

@RestController
public class FixedThreadPoolController {

    private final ExecutorService fixedThreadPool;

    public FixedThreadPoolController(ExecutorService fixedThreadPool) {
        this.fixedThreadPool = fixedThreadPool;
    }

    @GetMapping("/fixed")
    public String processFixedTasks() {

        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            fixedThreadPool.submit(() ->
                    System.out.println("Fixed Task " + taskId +
                            " executed by " + Thread.currentThread().getName())
            );
        }
        return "Fixed thread pool tasks submitted";
    }
}
Enter fullscreen mode Exit fullscreen mode

CURL

curl http://localhost:8080/fixed
Enter fullscreen mode Exit fullscreen mode

Response

Fixed thread pool tasks submitted
Enter fullscreen mode Exit fullscreen mode

2️⃣ CachedThreadPool – Short-Lived Async Tasks

When to Use

  • Burst traffic
  • Short tasks
  • Lightweight async work

⚠️ Be careful: unbounded threads


REST API Example

@RestController
public class CachedThreadPoolController {

    private final ExecutorService cachedThreadPool;

    public CachedThreadPoolController(ExecutorService cachedThreadPool) {
        this.cachedThreadPool = cachedThreadPool;
    }

    @GetMapping("/cached")
    public String processCachedTasks() {

        for (int i = 1; i <= 5; i++) {
            cachedThreadPool.submit(() ->
                    System.out.println("Cached task by " +
                            Thread.currentThread().getName())
            );
        }
        return "Cached thread pool tasks submitted";
    }
}
Enter fullscreen mode Exit fullscreen mode

CURL

curl http://localhost:8080/cached
Enter fullscreen mode Exit fullscreen mode

Response

Cached thread pool tasks submitted
Enter fullscreen mode Exit fullscreen mode

3️⃣ SingleThreadExecutor – Sequential Execution

When to Use

  • Order must be preserved
  • One task at a time
  • Logging, auditing, batch steps

REST API Example

@RestController
public class SingleThreadController {

    private final ExecutorService singleThreadExecutor;

    public SingleThreadController(ExecutorService singleThreadExecutor) {
        this.singleThreadExecutor = singleThreadExecutor;
    }

    @GetMapping("/single")
    public String processSequentialTasks() {

        for (int i = 1; i <= 3; i++) {
            int taskId = i;
            singleThreadExecutor.submit(() ->
                    System.out.println("Sequential task " + taskId +
                            " executed by " + Thread.currentThread().getName())
            );
        }
        return "Single-thread tasks submitted";
    }
}
Enter fullscreen mode Exit fullscreen mode

CURL

curl http://localhost:8080/single
Enter fullscreen mode Exit fullscreen mode

Response

Single-thread tasks submitted
Enter fullscreen mode Exit fullscreen mode

✔ Tasks always execute in order


4️⃣ ScheduledThreadPool – Delayed or Periodic Tasks

When to Use

  • Scheduled jobs
  • Polling
  • Cleanup tasks

REST API Example

@RestController
public class ScheduledThreadPoolController {

    private final ScheduledExecutorService scheduledExecutor;

    public ScheduledThreadPoolController(ScheduledExecutorService scheduledExecutor) {
        this.scheduledExecutor = scheduledExecutor;
    }

    @GetMapping("/schedule")
    public String scheduleTask() {

        scheduledExecutor.schedule(
                () -> System.out.println("Scheduled task executed"),
                5,
                java.util.concurrent.TimeUnit.SECONDS
        );

        return "Task scheduled to run after 5 seconds";
    }
}
Enter fullscreen mode Exit fullscreen mode

CURL

curl http://localhost:8080/schedule
Enter fullscreen mode Exit fullscreen mode

Response

Task scheduled to run after 5 seconds
Enter fullscreen mode Exit fullscreen mode

Best Practices for Thread Pools

✅ 1. Match Pool Type to Workload

Wrong pool = production issue.


✅ 2. Prefer FixedThreadPool for APIs

Predictable and safe.


✅ 3. Always Shutdown Executors (on app exit)

Avoid memory leaks.


❌ Common Mistakes to Avoid

🚫 Using CachedThreadPool for heavy traffic
🚫 Blocking threads unnecessarily
🚫 Ignoring queue behavior
🚫 Creating thread pools per request


Conclusion

Thread pools are not interchangeable.

Each type exists for a reason:

  • Fixed → controlled concurrency
  • Cached → short async bursts
  • Single → strict ordering
  • Scheduled → time-based execution

Understanding when—and why—to use each one is a must-have skill for every Java developer.


Call to Action 🚀

💬 Which thread pool do you use most in real projects?
Ask questions or share experiences in the comments.

📌 Follow for more Java programming, concurrency, and Spring Boot deep dives.


🔗 Authoritative References

Top comments (0)