If you've ever wondered how apps handle multiple tasks at once without freezing — like loading images while playing music — thread pools are the answer. This guide explains everything in plain language with real examples.
First, What Is a Thread?
Think of a thread as a worker in a restaurant kitchen. Your app is the restaurant. By default, your Android app has one main thread — one cook doing everything: taking orders, cooking food, washing dishes.
That works fine for small tasks. But if one task takes too long (like a slow network call), everything else stops — the UI freezes, the user sees an ANR (App Not Responding) error. Not good.
The solution? Hire more cooks — create more threads.
What Is a Thread Pool?
Instead of hiring (creating) a new cook every time an order (task) comes in and firing them when done, a thread pool keeps a team of cooks on standby, ready to take the next task as soon as one finishes.
Real-world analogy: Imagine a call center with 5 customer service agents. Instead of hiring a new agent for every call and firing them when the call ends, the same 5 agents handle all incoming calls. When all 5 are busy, new calls wait in a queue. When an agent finishes, they pick up the next call in the queue.
That call center is a thread pool.
Benefits:
- No overhead from creating/destroying threads repeatedly
- Controlled number of threads (prevents memory overload)
- Tasks queue up and execute automatically
What Are Worker Threads?
Worker threads are the actual threads inside the pool — the individual agents in the call center.
Here is how they work:
- A task is submitted to the pool
- An available worker thread picks it up
- The worker executes the task
- When done, the worker goes back to the pool and picks up the next task
- If no tasks are waiting, the worker sits idle
// Without thread pool — creates a NEW thread every time (wasteful) Thread { // do some work }.start() // With thread pool — reuses existing threads (efficient) val executor = Executors.newFixedThreadPool(4) executor.execute { // do some work }
Types of Thread Pools in Java/Kotlin
Java provides ready-made thread pools through the Executors class. There are three main types.
class. There are three main types.
1. Fixed Thread Pool
A fixed thread pool has a set number of threads that never changes.
Analogy: A restaurant with exactly 4 waiters. No more, no less. If all 4 are busy, new customers wait at the door until a waiter is free.
When to use it:
- When you know roughly how many concurrent tasks you'll have
- CPU-intensive tasks (image processing, data crunching)
- When you want predictable resource usage
Sample output:import java.util.concurrent.Executors fun main() { // Create a pool with exactly 4 threads val executor = Executors.newFixedThreadPool(4) // Submit 6 tasks — only 4 run at a time, 2 wait in queue for (i in 1..6) { executor.execute { println("Task $i running on thread: ${Thread.currentThread().name}") Thread.sleep(1000) // simulate work println("Task $i done") } } executor.shutdown() // stop accepting new tasks, finish current ones }
Notice tasks 5 and 6 reuse the same threads — that is the whole point.Task 1 running on thread: pool-1-thread-1 Task 2 running on thread: pool-1-thread-2 Task 3 running on thread: pool-1-thread-3 Task 4 running on thread: pool-1-thread-4 Task 1 done Task 5 running on thread: pool-1-thread-1 ← thread reused! Task 2 done Task 6 running on thread: pool-1-thread-2 ← thread reused!
2. Cached Thread Pool
A cached thread pool creates new threads as needed but reuses idle threads when available. Idle threads that have been unused for 60 seconds are automatically removed.
Analogy: A taxi company with no fixed fleet. When demand is low, only 2 taxis are on the road. During rush hour, 20 taxis come out. After rush hour, the extra taxis park and eventually go home if not needed.
When to use it:
- Many short-lived tasks
- Unpredictable workload that spikes and drops
- I/O tasks (network requests, file reads) where threads spend a lot of time waiting
import java.util.concurrent.Executors fun main() { // Pool starts with 0 threads, grows as needed, shrinks when idle val executor = Executors.newCachedThreadPool() println("Submitting 5 quick tasks...") for (i in 1..5) { executor.execute { println("Task $i started on: ${Thread.currentThread().name}") Thread.sleep(500) // short task println("Task $i finished") } } Thread.sleep(2000) // wait for tasks to finish println("\nSubmitting 2 more tasks (threads may be reused)...") for (i in 6..7) { executor.execute { println("Task $i running on: ${Thread.currentThread().name}") } } executor.shutdown() }⚠️ Warning: If you submit millions of tasks rapidly, a cached pool will create millions of threads and crash your app. Use with care.
3. Scheduled Thread Pool
A scheduled thread pool runs tasks at a specific time or repeatedly at fixed intervals.
Analogy: A school bell system. The bell rings at 8:00 AM to start school, again at 8:45 to change class, again at 8:50 when the next class starts. It does not ring once and stop — it keeps repeating on a schedule.
When to use it:
- Polling an API every 30 seconds
- Clearing a cache every hour
- Running a health check at fixed intervals
- Sending reminders or notifications on a schedule
import java.util.concurrent.Executors import java.util.concurrent.TimeUnit fun main() { // Create scheduled pool with 2 threads val scheduler = Executors.newScheduledThreadPool(2) // Run ONCE after a 2-second delay scheduler.schedule({ println("One-time task ran after 2 seconds delay") }, 2, TimeUnit.SECONDS) // Run REPEATEDLY — first run after 1 second, then every 3 seconds scheduler.scheduleAtFixedRate({ println("Repeated task ran at: ${System.currentTimeMillis()}") }, 1, 3, TimeUnit.SECONDS) // Let it run for 10 seconds then stop Thread.sleep(10000) scheduler.shutdown() println("Scheduler stopped") }Sample output:
One-time task ran after 2 seconds delay Repeated task ran at: 1718000001000 Repeated task ran at: 1718000004000 Repeated task ran at: 1718000007000 Repeated task ran at: 1718000010000 Scheduler stopped
Quick Comparison Table
| Type | Threads | Best For | Risk |
|---|---|---|---|
| Fixed | Fixed number | CPU-heavy tasks, predictable load | Tasks queue up if all busy |
| Cached | Grows/shrinks automatically | Many short I/O tasks | Can create too many threads |
| Scheduled | Fixed number | Repeating/delayed tasks | Missed executions if thread is busy |
Thread Pools in Android (Modern Approach)
In Android, you rarely use Executors directly. Instead, Kotlin Coroutines manage thread pools for you under the hood.
// viewModelScope uses a thread pool internally
viewModelScope.launch(Dispatchers.IO) {
// Dispatchers.IO uses a cached thread pool (up to 64 threads)
val data = repository.fetchData()
}
viewModelScope.launch(Dispatchers.Default) {
// Dispatchers.Default uses a fixed thread pool (CPU core count threads)
val result = heavyComputation()
}
| Dispatcher | Pool Type | Use For |
|---|---|---|
Dispatchers.IO |
Cached (up to 64 threads) | Network, file, database |
Dispatchers.Default |
Fixed (= CPU cores) | Heavy computation |
Dispatchers.Main |
Single (main thread) | UI updates |
Common Mistakes to Avoid
1. Not shutting down the executor
// ❌ Wrong — pool keeps running forever, leaks memory
val executor = Executors.newFixedThreadPool(4)
executor.execute { doWork() }
// forgot executor.shutdown()
// ✅ Correct
executor.shutdown() // graceful shutdown after tasks complete
// or
executor.shutdownNow() // cancel all pending tasks immediately
2. Doing UI work inside a thread pool
// ❌ Wrong — crashes with CalledFromWrongThreadException
executor.execute {
val data = fetchData()
textView.text = data // UI update on background thread!
}
// ✅ Correct — post back to main thread
executor.execute {
val data = fetchData()
runOnUiThread { textView.text = data }
}
3. Using cached pool for heavy CPU tasks
// ❌ Wrong — too many threads competing for CPU
val executor = Executors.newCachedThreadPool()
for (i in 1..100) {
executor.execute { heavyCpuWork() } // creates 100 threads!
}
// ✅ Correct — limit threads to CPU core count
val cores = Runtime.getRuntime().availableProcessors()
val executor = Executors.newFixedThreadPool(cores)
Summary
- A thread pool keeps a set of pre-created threads ready to run tasks — no wasteful create/destroy cycle
- Worker threads are the threads inside the pool that actually execute your tasks and go back for more when done
- Fixed pool — best when load is predictable and tasks are CPU-heavy
- Cached pool — best for many short-lived I/O tasks with unpredictable bursts
- Scheduled pool — best for tasks that need to run on a timer or repeat at intervals
- In modern Android, Kotlin Coroutines with Dispatchers manage thread pools for you, making concurrent code safer and cleaner
Understanding thread pools makes you a better developer — you stop guessing why your app is slow or why background work crashes, and start making deliberate, informed decisions.
Happy coding!
Comments (0)