In Android, Thread and Handler are two fundamental structures used together in multithreading management.
In Android, UI operations can only be performed on the main thread (UI thread). For background operations (network requests, database operations, file read/write), you need to create separate threads. However, these threads cannot update the UI directly.
Handler is a mechanism that allows you to send/process messages and runnable objects between different threads. It is typically used to send data from a background thread to the UI thread.
public class MainActivity extends AppCompatActivity {
// Handler created on the UI thread
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// This code runs on the UI thread
switch (msg.what) {
case 1:
String data = msg.getData().getString("key");
textView.setText(data);
break;
}
}
};
private void startBackgroundWork() {
new Thread(new Runnable() {
@Override
public void run() {
// This code runs on the background thread
String result = doHeavyWork();
// Send a message to the Handler to update the UI
Message message = new Message();
message.what = 1;
Bundle bundle = new Bundle();
bundle.putString("key", result);
message.setData(bundle);
uiHandler.sendMessage(message);
}
}).start();
}
}
public class MainActivity extends AppCompatActivity {
private Handler uiHandler = new Handler(Looper.getMainLooper());
private void startBackgroundWork() {
new Thread(new Runnable() {
@Override
public void run() {
// Background operation
String result = doHeavyWork();
// Post the UI update to the UI thread
uiHandler.post(new Runnable() {
@Override
public void run() {
// This code runs on the UI thread
textView.setText(result);
}
});
}
}).start();
}
}
HandlerThread is a thread that has its own message queue and looper. It is ideal for long-lived background operations.
public class MyActivity extends AppCompatActivity {
private Handler backgroundHandler;
private HandlerThread backgroundThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create and start HandlerThread
backgroundThread = new HandlerThread("MyBackgroundThread");
backgroundThread.start();
// This Handler runs on the background thread
backgroundHandler = new Handler(backgroundThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
// This code runs on the BACKGROUND thread
switch (msg.what) {
case 1:
doHeavyWork();
break;
}
}
};
}
private void startBackgroundTask() {
// Send a message to the background thread
backgroundHandler.sendEmptyMessage(1);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Clean up the thread
backgroundThread.quitSafely();
}
}
Looper is a loop that processes a thread’s message queue. Not every thread has a looper.
- UI Thread: Automatically has a Looper
- Normal Thread: Does not have a Looper by default
// Creating a thread with its own Looper
class LooperThread extends Thread {
public Handler mHandler;
@Override
public void run() {
// Prepare the Looper
Looper.prepare();
// Create the Handler
mHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
// Process messages
}
};
// Start the loop
Looper.loop();
}
}
You can also perform delayed operations with Handler:
// Operation that will run after 3 seconds
uiHandler.postDelayed(new Runnable() {
@Override
public void run() {
// Delayed operation
textView.setText("3 seconds have passed");
}
}, 3000);
// Cancel the timer
uiHandler.removeCallbacksAndMessages(null);
In modern Android development, coroutines are preferred over raw Thread + Handler:
lifecycleScope.launch(Dispatchers.IO) {
val result = doHeavyWork()
withContext(Dispatchers.Main) {
textView.text = result
}
}
| Component | Task | Thread it runs on |
|---|---|---|
| Thread | Runs background operations | Its own thread |
| Handler | Sends/processes messages and runnables | The thread it was created on |
| Looper | Processes the message queue | The thread it resides in |
| Message | Object that carries data | – |
| Runnable | Code block to be executed | Handler’s thread |
Executors.newFixedThreadPool() is actually a pre-configured, simpler version of new ThreadPoolExecutor(). However, there are some critical differences in the background.
| Feature | new ThreadPoolExecutor(…) | Executors.newFixedThreadPool() |
|---|---|---|
| Core pool size | You can set any value | nThreads (3) |
| Max pool size | You can set any value | nThreads (3) (same as core) |
| Keep alive time | You specify (idle time for non-core threads) | 0 (zero) – because max = core, so no non-core threads |
| Time unit | You choose (seconds, milliseconds, etc.) | TimeUnit.MILLISECONDS |
| Queue type | Any BlockingQueue (e.g., PriorityBlockingQueue) | Unlimited LinkedBlockingQueue |
| Thread factory | Optional (custom thread naming, etc.) | Default DefaultThreadFactory |
| Rejection policy | You choose (AbortPolicy, CallerRunsPolicy, etc.) | Default AbortPolicy (throws error when queue is full) |
newFixedThreadPool() uses an unbounded queue (LinkedBlockingQueue).
- If all n threads are busy, new tasks are added to the queue.
- Memory risk: If tasks are added too quickly and cannot be processed, the queue can grow indefinitely → you may get OutOfMemoryError.
With new ThreadPoolExecutor(), you can control this risk by specifying a bounded queue (e.g., ArrayBlockingQueue(10)).
newFixedThreadPool(n):
- Fixed number of threads. No extra threads are created beyond the need.
- If the number of tasks increases, only the queue grows.
With new ThreadPoolExecutor(), you can configure something like this:
corePoolSize = 2
maxPoolSize = 5
keepAliveTime = 30 seconds
Queue = ArrayBlockingQueue(10)
In this case:
- 2 threads are always alive.
- Tasks go to the queue until it is full.
- When the queue is full, new threads are created up to a maximum of 5.
- Threads idle for 30 seconds are terminated.
This flexibility is not possible with newFixedThreadPool().
- For simple, fixed-number operations (e.g., 3 sequential network requests) →
Executors.newFixedThreadPool(3)is sufficient. - For cases where there is a risk of queue overflow and you want to control resources → Use
new ThreadPoolExecutor()to limit the queue and add a rejection policy.
// 1. Ready-made fixed thread pool:
ExecutorService fixed = Executors.newFixedThreadPool(3);
// 2. Writing the same behavior manually (this is what newFixedThreadPool does internally):
ExecutorService manual = new ThreadPoolExecutor(
3, 3,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>() // ← Unbounded queue!
);
// 3. More controlled, bounded queue version:
ExecutorService controlled = new ThreadPoolExecutor(
2, 4,
30L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), // ← At most 10 tasks are queued
new ThreadPoolExecutor.CallerRunsPolicy() // ← If queue is full, the calling thread does the work
);
Executors.newFixedThreadPool(n)– Good for code simplicity and fixed number of threads, but can lead to memory overflow due to unbounded queue.new ThreadPoolExecutor()– Allows you to control all parameters: thread counts, queue type and size, wait times, rejection policies. Should be preferred for critical systems and in Android (due to limited RAM on mobile devices).
When the queue defined for ThreadPoolExecutor() is full, you can add a rejection policy to determine what happens when the queue is full:
mTaskQueue = new LinkedBlockingQueue<Runnable>(100);
mForBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES,
NUMBER_OF_CORES * 5,
1,
TimeUnit.SECONDS,
mTaskQueue,
new BackgroundThreadFactory(),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// Have the calling thread wait and do the work (backpressure)
if (!executor.isShutdown()) {
try {
executor.getQueue().put(r); // This is blocking!
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
);
Or you can use one of Java’s built-in policies:
// Option A: When queue is full, let the calling thread do the work (creates backpressure)
mForBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES,
NUMBER_OF_CORES * 5,
1,
TimeUnit.SECONDS,
mTaskQueue,
new BackgroundThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // ← Run on the calling thread
);
// Option B: When queue is full, discard the oldest task and add the new one
mForBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES,
NUMBER_OF_CORES * 5,
1,
TimeUnit.SECONDS,
mTaskQueue,
new BackgroundThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy() // ← Delete the oldest task
);
// Option C: Silently reject (the task is lost - be careful with this!)
mForBackgroundTasks = new ThreadPoolExecutor(
NUMBER_OF_CORES,
NUMBER_OF_CORES * 5,
1,
TimeUnit.SECONDS,
mTaskQueue,
new BackgroundThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy() // ← Ignore the task
);