androidinterview.com

Android Threading Interview Questions

17 questions

Tier
Difficulty
Level

Showing all 17 questions

Threads, Handlers & Loopers

Handler, Looper and MessageQueue are almost always asked together, as one chain.

What is the difference between the UI thread and a background thread?

Tier: EssentialDifficulty: Easy

The UI thread, also called the main thread, is the one thread the system creates that's allowed to touch View objects, and it's the only one running a Looper from the moment your app starts, pumping the MessageQueue that drives every click, animation, and lifecycle callback. A background thread is any other thread, whether one you create directly or one pulled from a pool, and it can do heavy work but can never touch a View directly.

Thread {
    val result = doExpensiveWork()          // fine, off the UI thread
    // textView.text = result               // would crash, wrong thread
    runOnUiThread { textView.text = result } // hop back to the UI thread to touch the view
}.start()

Anything that blocks the UI thread for more than a moment, network calls, disk reads, heavy computation, shows up as dropped frames or, past about five seconds, an ANR. That's the entire reason background threads exist, to keep the UI thread free to keep pumping that queue at 60 or more frames a second. The corresponding rule going the other way is that a background thread can never touch a View directly, since the view system isn't thread safe, results have to be handed back to the UI thread through a Handler, runOnUiThread, or withContext(Dispatchers.Main) in coroutines.

Explain Thread, Handler, Looper, MessageQueue and HandlerThread.

Tier: EssentialDifficulty: Medium

These five pieces are how Android moves work onto and off of a given thread, and they only make sense together.

  • Thread. A basic unit of execution. Android's main thread is just a Thread with one extra thing attached to it, a Looper.
  • MessageQueue. A queue of Message and Runnable objects waiting to be processed, owned by one thread.
  • Looper. Runs on a thread and continuously pulls the next item off that thread's MessageQueue, dispatching it for execution. Without a Looper, a thread has nowhere to receive posted work, it just runs its run() method and exits.
  • Handler. Your entry point for posting work into a specific thread's queue. You create a Handler tied to a Looper, and calling handler.post { ... } from any thread queues that block to run on the Handler's thread.
  • HandlerThread. A Thread subclass that sets up its own Looper for you, since a plain background Thread doesn't have one by default. You start it, grab its Looper, and hand that to a Handler to get a dedicated background thread you can keep posting work to.

The classic use is posting a result from a background thread back to the main thread. Handler(Looper.getMainLooper()).post { textView.text = result } works because the main thread already has a Looper running from the moment the app starts, pumping the MessageQueue that drives every UI update, click, and lifecycle callback. In modern code coroutines usually replace this, withContext(Dispatchers.Main) is doing the same handoff under the hood, but the interview question is really checking that you understand what's happening beneath that abstraction.

What is an ANR? How can an ANR be prevented?

Tier: EssentialDifficulty: Medium

An ANR, Application Not Responding, is what the system throws up when your app's main thread is blocked for too long to respond to input, and it offers the user a dialog to force quit you.

Android is watching for a few specific triggers, not just "the app feels slow."

  • No response to a key press or touch within 5 seconds.
  • A BroadcastReceiver's onReceive() not finishing within 5 seconds while the app is in the foreground.
  • A Service not calling startForeground() within 5 seconds of Context.startForegroundService().
  • Service.onCreate() or onStartCommand() taking too long to return.

Prevention comes down to one rule, never block the main thread, and it plays out as several concrete habits.

  • Move network calls, disk reads and writes, and database queries off the main thread, typically with Dispatchers.IO in a coroutine.
  • Keep BroadcastReceiver.onReceive() short, if the work takes real time hand it off to a coroutine, WorkManager, or a Service instead of doing it inline.
  • Avoid locks on the main thread that a background thread might hold, since a blocked main thread waiting on a mutex is exactly what an ANR looks like.
  • Use StrictMode during development to catch accidental disk or network access on the main thread before it ships.

If you do ship an ANR, Android vitals in Play Console and ApplicationExitInfo on API 30 and up will tell you which one hit users and how often, which is usually where a real investigation starts.

What are the different types of threads in Android and how do they differ?

Tier: CommonDifficulty: Easy

An Android app deals with a handful of distinct kinds of threads, and what tells them apart is who creates them and what they're allowed to touch.

  • The main thread, also called the UI thread. There's exactly one per app, it's created by the system, and it's the only thread allowed to touch View objects. It runs a Looper pumping a MessageQueue, which is how every click, animation frame, and lifecycle callback actually gets dispatched.
  • Background or worker threads. Plain Thread instances, or threads pulled from a pool you create yourself, for offloading network calls, disk I/O, or heavy computation. They can't touch views directly, results have to be posted back to the main thread.
  • HandlerThread. A background thread that sets up its own Looper, giving you a dedicated thread you can keep posting ordered work to through a Handler.
  • Binder threads. A pool the system manages for you, used to handle incoming cross process calls, like a ContentProvider query from another app or an AIDL call. Code running in these callbacks is not on your main thread, even though it feels like a normal method call.

The one that trips people up is Binder threads, because a ContentProvider.query() or an AIDL method looks like an ordinary function you wrote, but it can run on a thread you never created and don't control, which matters if that code touches shared state without synchronization.

What is the difference between a Runnable and a Thread in Android?

Tier: CommonDifficulty: Easy

A Runnable is just a task, an interface with one method, run(), describing what to do. A Thread is an actual unit of execution that can run that task. A Runnable on its own does nothing, it needs a Thread, a Handler, or an executor to actually call run() on it.

val task = Runnable { println("doing work") }   // just describes the work

Thread(task).start()                            // one thread runs it
Handler(myLooper).post(task)                    // or a Handler's thread runs it
executor.execute(task)                          // or a pool thread runs it

You can also subclass Thread directly and override run(), but that means the class is now permanently tied to being a thread, and since Java only allows single inheritance, extending Thread uses up the one base class slot the object gets. Implementing Runnable instead keeps the task as a plain object you can hand to a Thread, a Handler, or an ExecutorService, and the same Runnable instance can even be reused across several of them.

That reusability is the practical reason Runnable wins almost every time in real code. A thread pool needs tasks it can queue and hand off to whichever worker thread is free next, and that only works if the task is decoupled from any specific thread, which is exactly what a Runnable gives you.

How do you know when a process is blocking the UI thread?

Tier: CommonDifficulty: Medium

You catch a blocked UI thread through a mix of logcat warnings, developer tooling, and the app just visibly stuttering, in roughly this order of how early they warn you.

  • Logcat prints a Choreographer warning like "Skipped 42 frames, the application may be doing too much work on its main thread" whenever a frame takes longer than about 16 milliseconds to render at 60fps.
  • StrictMode, enabled in debug builds, flags the exact offending call the moment it happens, catching things like disk reads, network calls, or database queries made on the main thread before they ever cause a visible stutter.
  • The GPU rendering profile bars in Developer Options give you a live, visual readout of every frame, so a spike above the green line points straight at the slow frame.
  • Android Studio's Profiler, or a Perfetto trace, shows exactly what the main thread was doing during a slow stretch, down to the method.
  • Past about five seconds of a genuinely unresponsive main thread, the system itself steps in and shows the user an ANR dialog, and writes a trace to /data/anr/traces.txt with the full stack of every thread at that moment.

The cheapest habit that catches most of this before it ships is just leaving StrictMode on in every debug build, since it points at the exact line the moment the violation happens, instead of you reconstructing it later from a dropped frame or an ANR trace.

Executors & Thread Pools

What are the advantages of a thread pool?

Tier: CommonDifficulty: Easy

A thread pool reuses a fixed set of worker threads across many tasks instead of spinning up and tearing down a new Thread for every single one, and that saves you real cost in a few different ways.

  • Creating a thread is expensive, it means allocating a call stack and doing OS level bookkeeping, so reusing threads avoids paying that cost per task.
  • A pool bounds how many threads can run at once, which keeps a burst of work from exhausting memory or CPU by spawning hundreds of threads at the same time.
  • Excess work waits in a queue instead of getting dropped or overwhelming the system, so throughput stays predictable under load.
  • You get cancellation, scheduling, and rejection handling for free through the ExecutorService API, instead of hand rolling that logic around raw threads.

In practice this is why almost nothing in modern Android code creates a Thread directly. Executors.newFixedThreadPool(), coroutine dispatchers like Dispatchers.IO, and WorkManager are all thread pools under the hood, doing this reuse and bounding for you.

What is a ThreadPoolExecutor?

Tier: CommonDifficulty: Medium

ThreadPoolExecutor is the concrete class behind ExecutorService, it manages a pool of worker threads that pull tasks off a queue and run them, and every factory method on Executors is really just a ThreadPoolExecutor configured with different numbers.

new ThreadPoolExecutor(
    2,                          // corePoolSize, threads kept alive even when idle
    4,                          // maximumPoolSize, the hard cap under load
    60, TimeUnit.SECONDS,       // keepAliveTime, how long an idle thread above core waits before dying
    new LinkedBlockingQueue<>() // workQueue, holds tasks once corePoolSize threads are all busy
);
  • corePoolSize is the number of threads the pool keeps running even when there's no work, so the next task doesn't pay thread creation cost.
  • maximumPoolSize is the ceiling. New threads beyond corePoolSize only get created once the queue is full, and even then the pool won't go past this number.
  • keepAliveTime and its TimeUnit control how long a thread above corePoolSize sits idle before it's let go.
  • workQueue is the BlockingQueue holding tasks that are waiting because every core thread is busy.

Knowing these four parameters is really what the question is testing, because it's what separates newFixedThreadPool(), which sets core and max to the same number with an unbounded queue, from newCachedThreadPool(), which sets core to zero and max to effectively unlimited with no queue at all, so it just keeps creating threads instead of making tasks wait.

Java Concurrency Primitives

What does the keyword synchronized mean?

Tier: CommonDifficulty: Easy

synchronized marks a method or a block of code so that only one thread can execute it at a time, by making the thread acquire a lock on an object before entering and release it on the way out.

public synchronized void increment() {   // locks on `this`
    count++;
}

public static synchronized void reset() { // locks on the Class object, shared by all instances
    total = 0;
}

public void addItem(Item item) {
    synchronized (lockObject) {           // locks on an explicit object, block scoped
        items.add(item);
    }
}

An instance method marked synchronized locks on this, so two threads calling it on the same object serialize, but two threads calling it on two different objects don't block each other at all. A static synchronized method locks on the Class object instead, which is shared across every instance, so it serializes access class wide. The block form, synchronized (someObject) { }, lets you lock on any object you choose and limit the locked region to just the lines that actually need protecting, instead of the whole method.

The tradeoff is that a thread blocked waiting for a lock just sits there, so a synchronized block that does anything slow, like I/O, turns into a bottleneck for every other thread waiting on it. That's why on Android you generally reach for it to protect a small piece of shared mutable state, not to wrap a network call.

What is the difference between concurrency and parallelism?

Tier: CommonDifficulty: Easy

Concurrency is about structuring a program to deal with multiple tasks that are in progress at overlapping times, parallelism is about those tasks literally running at the same instant on different cores. A single CPU core can be concurrent, rapidly switching between tasks, without ever being parallel.

Think of one person juggling several conversations by switching between them quickly, that's concurrency. Two people each having their own conversation at the same time, that's parallelism. A single core running coroutines is doing the first, suspending one and resuming another so fast it looks simultaneous. A four core CPU actually running four coroutines at once, one per core, is doing the second.

On Android this distinction shows up directly in dispatcher choice. Dispatchers.Default is backed by a thread pool sized to the number of CPU cores, so CPU bound coroutines launched on it can run in true parallelism. Dispatchers.IO is backed by a much larger pool, because its threads spend most of their time blocked waiting on I/O rather than using the CPU, so it's optimizing for concurrency, keeping many tasks in flight, rather than for parallel CPU throughput.

Answer a twisted question related to ConcurrentModificationException in an ArrayList.

Tier: CommonDifficulty: MediumAsked at: booking-com

The twist interviewers like is that removing the second to last element inside a for each loop does not throw ConcurrentModificationException, even though removing any other element does.

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : list) {
    if (n == 3) {
        list.remove(n); // second to last element, no exception
    }
}

ArrayList's iterator is fail fast. Its next() method checks a modCount field against the value it captured at creation, and throws if they no longer match. But that check only runs inside next(), not inside hasNext(). Removing an element shifts everything after it down by one and decrements size, without touching the iterator's cursor. If you remove the second to last item, the cursor now equals the new, smaller size, so hasNext() returns false and the loop ends before next() is ever called again. The check that would have caught the modification never runs.

Remove any other element and there is still at least one more next() call pending, which does run the check and throws as expected. This is exactly why removing an element while iterating a plain for each loop is unsafe in the first place, the exception just happens to not fire in this one specific position.

The real fix is to never mutate a list through a reference other than the iterator itself while iterating it.

  • Use Iterator.remove() directly, which updates the iterator's own state.
  • Use Collections.synchronizedList or CopyOnWriteArrayList if the mutation comes from another thread.
  • Build a filtered copy with removeIf() or a stream instead of mutating in place.

What is the volatile modifier?

Tier: CommonDifficulty: Medium

volatile guarantees that a write to a field on one thread is immediately visible to every other thread reading it, without guaranteeing that operations on that field are atomic.

@Volatile
private var isRunning = true

fun stop() {
    isRunning = false // this write is immediately visible on every other thread
}

fun workerLoop() {
    while (isRunning) { // always reads the latest value, never a stale cached copy
        doWork()
    }
}

Without volatile, a thread can cache a field's value in a CPU register or its own core's cache, and never notice another thread wrote a new value, so a loop checking isRunning could spin forever even after stop() ran. Marking the field volatile forces every read to go back to main memory and every write to flush there immediately, and it also stops the compiler and CPU from reordering instructions around that access.

What it does not do is make compound operations atomic. count++ on a volatile field is still a read, an increment, and a write as three separate steps, and two threads can interleave those steps and lose an update, the exact same race you'd get without volatile at all. For that you need AtomicInteger, or a synchronized block. volatile is the right tool for a single flag or reference that one thread writes and others only read, not for anything more than one thread updates.

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Threads, Handlers & Loopers

What is the difference between daemon threads and user threads?

Tier: Less commonDifficulty: Easy

A user thread keeps the JVM alive, a daemon thread does not. The JVM shuts down the moment every user thread has finished, and at that point it kills off any daemon threads still running, mid task, without waiting for them.

Thread worker = new Thread(() -> longRunningTask());
worker.setDaemon(true); // must be called before start(), or it throws IllegalThreadStateException
worker.start();

Every thread you create is a user thread by default, setDaemon(true) is what opts it into daemon behavior, and it only takes effect if called before start(). Garbage collection and JIT compiler threads inside the JVM itself are daemon threads, they exist to serve the running program, not to be the reason it stays running.

The practical implication is that a daemon thread is only appropriate for work you're fine with getting abandoned. On Android this distinction matters less day to day, since the process, not thread type, is what the system kills, but it's still the right mental model for any background thread you spin up yourself. If the work has to complete, like flushing a write to disk, it needs to be a user thread, or better, tracked by something that survives the calling scope, like WorkManager.

Executors & Thread Pools

What are the different methods of concurrency on Android? Compare ExecutorService, CachedThreadPool, FixedThreadPool, AsyncTask and HandlerThread.

Tier: Less commonDifficulty: Medium

These are the classic pre coroutine tools for getting work off the main thread, and they each trade off differently between control, resource usage, and how easy they are to get wrong.

val fixedPool = Executors.newFixedThreadPool(4)   // bounded pool, predictable resource use
val cachedPool = Executors.newCachedThreadPool()   // unbounded pool, grows and shrinks on demand
val handlerThread = HandlerThread("worker").apply { start() }
val handler = Handler(handlerThread.looper)        // serial queue on one dedicated background thread
  • ExecutorService is the interface all of these implement, giving you submit(), execute(), and shutdown control over a pool of worker threads.
  • Executors.newFixedThreadPool(n) keeps exactly n threads alive and queues any work beyond that. Predictable memory and CPU use, good for CPU bound work you want to throttle.
  • Executors.newCachedThreadPool() creates a new thread for each task if none are idle, and reuses idle threads after that, killing them off after 60 seconds of inactivity. Great for a burst of many short lived tasks, risky under sustained heavy load since nothing caps how many threads it can create.
  • AsyncTask is deprecated since API 30 and should not be used in new code. It ran a single background thread by default through a serial executor, and it's notorious for leaking the Activity or Fragment it was created in if the screen was destroyed before it finished. Coroutines with viewModelScope, or WorkManager for anything that needs to survive process death, replace it entirely.
  • HandlerThread gives you one dedicated background thread with its own Looper, so work you post to its Handler runs serially, in order, on that same thread every time. That ordering guarantee is the one thing a thread pool doesn't give you, which is why it's still the right tool for something like a single ordered stream of sensor events.

If you're starting new code today, reach for coroutines first. withContext(Dispatchers.IO) covers what ExecutorService did, and a HandlerThread is still the honest answer whenever you specifically need serial ordering on a background thread.

Java Concurrency Primitives

Explain monitors and synchronization in Java.

Tier: Less commonDifficulty: Medium

A monitor is the lock built into every Java object, and synchronized is the keyword that acquires and releases it. Only one thread can hold a given object's monitor at a time, so any code that runs while holding it is effectively single threaded with respect to every other thread trying to get the same lock.

public class Counter {
    private int value = 0;

    public synchronized void increment() { // acquires the monitor on `this`
        value++;
    }

    public void incrementBlockForm() {
        synchronized (this) {              // same lock, block scoped instead of method scoped
            value++;
        }
    }
}

A thread that calls increment() acquires the monitor on entry and releases it on exit, including if the method throws. Any other thread calling increment() or incrementBlockForm() on the same instance blocks until the lock is free, because both forms lock on the same object, this.

Monitors also carry a wait set, which is where wait(), notify(), and notifyAll() come in. All three are methods on Object, not on Thread, and all three can only be called from inside a synchronized block on the object you're calling them on, otherwise you get an IllegalMonitorStateException. wait() releases the monitor and parks the calling thread until another thread calls notify() or notifyAll() on that same object, notify() wakes exactly one waiting thread, notifyAll() wakes all of them and lets them re-contend for the lock. This is the classic producer consumer pattern, a producer thread fills a buffer and calls notifyAll(), a consumer thread checks the buffer, calls wait() if it's empty, and resumes once woken.

What is the difference between an object-level lock and a class-level lock in Java?

Tier: Less commonDifficulty: Medium

An object level lock is acquired on a specific instance, a class level lock is acquired on the Class object itself, and because those are two different locks, a thread holding one does not block a thread trying to acquire the other.

public class Counter {
    public synchronized void increment() {        // object level lock, on `this`
        // ...
    }

    public static synchronized void resetTotal() { // class level lock, on Counter.class
        // ...
    }
}

increment() locks on whichever Counter instance it's called on, so two threads calling increment() on two separate Counter objects run concurrently, neither blocks the other. resetTotal(), being static, locks on the single Counter.class object, which is shared by every instance in the JVM, so it serializes access across all of them regardless of which instance, if any, is involved.

The mistake this catches is assuming that a static synchronized method and an instance synchronized method protect the same thing. They don't. A thread inside increment() on one instance and a thread inside resetTotal() at the same time run fully in parallel, holding two unrelated locks, even though both methods belong to the same class. If they're both meant to guard the same shared state, like a static counter also read from an instance method, you have to lock on the same object explicitly in both places, usually synchronized (Counter.class) in both, rather than relying on the default lock each form picks for you.

Describe get, set, lazySet, compareAndSet and weakCompareAndSet in the atomic package.

Tier: Less commonDifficulty: Hard

The classes in java.util.concurrent.atomic, like AtomicInteger and AtomicReference, give you thread safe reads and updates on a single variable without a synchronized block, using compare and swap instructions at the hardware level instead of a lock. These five methods are the vocabulary of that API.

AtomicInteger counter = new AtomicInteger(0);

counter.get();                              // read the current value, volatile semantics
counter.set(5);                             // write immediately, visible to all threads at once
counter.lazySet(10);                        // write, but visibility to other threads may lag
counter.compareAndSet(10, 20);              // if current value is 10, set to 20, returns boolean
while (!counter.weakCompareAndSet(20, 30)); // like compareAndSet, but may fail spuriously, retry yourself
  • get() reads the current value with the same visibility guarantee as a volatile read, you always see the latest write from any thread.
  • set(value) writes with the same guarantee as a volatile write, the new value is visible to every other thread immediately.
  • lazySet(value) writes the value but skips the memory barrier that forces immediate cross thread visibility. It's cheaper than set(), and the right choice when you know nothing else is reading the field right away, a field being nulled out for garbage collection is the classic example.
  • compareAndSet(expected, new) atomically updates the value to new only if it currently equals expected, and reports success as a boolean. This is the primitive that every lock free algorithm is built on, you read a value, compute the next one, and try to swap it in, retrying if someone else got there first.
  • weakCompareAndSet does the same compare and swap, but on some hardware it's allowed to fail even when the expected value does match, because it doesn't establish the same happens before ordering as compareAndSet. It maps more directly to the underlying load linked, store conditional instruction on some architectures, so it's cheaper, but only safe to use inside your own retry loop where an occasional spurious failure is harmless.