Android Processes and Management

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
    }
}
ComponentTaskThread it runs on
ThreadRuns background operationsIts own thread
HandlerSends/processes messages and runnablesThe thread it was created on
LooperProcesses the message queueThe thread it resides in
MessageObject that carries data
RunnableCode block to be executedHandler’s thread

Executors.newFixedThreadPool() is actually a pre-configured, simpler version of new ThreadPoolExecutor(). However, there are some critical differences in the background.

Featurenew ThreadPoolExecutor(…)Executors.newFixedThreadPool()
Core pool sizeYou can set any valuenThreads (3)
Max pool sizeYou can set any valuenThreads (3) (same as core)
Keep alive timeYou specify (idle time for non-core threads)0 (zero) – because max = core, so no non-core threads
Time unitYou choose (seconds, milliseconds, etc.)TimeUnit.MILLISECONDS
Queue typeAny BlockingQueue (e.g., PriorityBlockingQueue)Unlimited LinkedBlockingQueue
Thread factoryOptional (custom thread naming, etc.)Default DefaultThreadFactory
Rejection policyYou 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
);

Leave a Comment

Your email address will not be published. Required fields are marked *

4 × 3 =