Threads, Executors & Thread Pools Quiz
KOTLIN › JVM Concurrency
A thread is parked inside a blocking socket read. What state does a thread dump report?
- RUNNABLE, since BLOCKED means contention on a monitor lock
- BLOCKED, because it cannot proceed
- WAITING, since the read has no timeout
- TIMED_WAITING, because every socket read carries an implicit timeout
Answer: RUNNABLE, since BLOCKED means contention on a monitor lock
BLOCKED is reserved for monitor-lock contention. A thread in a blocking system call is reported RUNNABLE even though it is doing nothing, which makes IO-heavy dumps look busier than they are.
Why is Future.get() the limitation that drove the JVM towards other models?
- It blocks the caller, so composing async work wastes a pooled thread
- It can only be called once per future
- It returns null whenever the submitted task has not yet finished running
- It works only for Runnable, not Callable
Answer: It blocks the caller, so composing async work wastes a pooled thread
No completion callback means waiting means blocking, defeating the purpose of a pool. CompletableFuture fixed composition, and coroutines gave it sequential syntax.
What makes Executors.newCachedThreadPool() risky under a burst?
- Its queue holds nothing, so every task with no free thread creates a new one
- It queues indefinitely until the heap runs out
- It silently discards tasks beyond 64 threads
- It executes each submitted task on the calling thread once the pool is saturated
Answer: Its queue holds nothing, so every task with no free thread creates a new one
A SynchronousQueue has zero capacity, so thread creation is unbounded and Android reports OutOfMemoryError from pthread_create. The fixed pool fails the other way, via its unbounded queue.
A ThreadPoolExecutor has core 2, max 50, unbounded queue. How many threads run?
- Exactly 2, since the pool queues before growing and the queue never fills
- Up to 50, growing with the queue length
- Between 2 and 50, depending entirely on the configured keep-alive time value
- 50 immediately, allocated up front
Answer: Exactly 2, since the pool queues before growing and the queue never fills
Growth past core size is triggered only by a full queue, which an unbounded queue never reports. The maximum is dead configuration and the queue grows without bound instead.
Which rejection policy acts as a natural brake on a fast producer?
- CallerRunsPolicy, which runs the task on the submitting thread
- AbortPolicy, which throws immediately
- DiscardPolicy, which drops the task
- DiscardOldestPolicy, which quietly drops the head of the work queue
Answer: CallerRunsPolicy, which runs the task on the submitting thread
Running the task on the submitter blocks the producer for its duration, slowing submission to match capacity. The others either fail loudly or lose work silently.
A task waits 90ms on IO and computes for 10ms on a 4-core device. What pool size does the formula give?
- About 40, from cores times one plus the wait-to-compute ratio
- About 5, since CPU pools stay near the core count
- About 64, matching the IO dispatcher ceiling
- About 4, since adding threads beyond the core count cannot help at all
Answer: About 40, from cores times one plus the wait-to-compute ratio
Four cores times (1 + 9) is forty. A thread parked on a socket uses no CPU, so you want enough that some are always runnable when cores free up.
Your lambda passed to thenApply returns a CompletableFuture. What is the result type?
- CompletableFuture<CompletableFuture<T>>, meaning you wanted thenCompose
- CompletableFuture<T>, flattened automatically
- A compile error, since thenApply rejects future-returning lambdas outright
- CompletableFuture<T> completing after both stages
Answer: CompletableFuture<CompletableFuture<T>>, meaning you wanted thenCompose
thenApply is map and thenCompose is flatMap. The nested type is the standard symptom of picking the mapping variant when flattening was needed.
A task submitted with submit() throws and nobody calls get(). What happens?
- The exception is stored in the Future and lost silently
- It reaches the thread uncaught handler and kills the pool thread
- It is rethrown at the submission site
- The executor logs it and retries the task
Answer: The exception is stored in the Future and lost silently
submit captures the failure into the Future for retrieval at get(). Silent task failure in production is nearly always this. execute has nowhere to store it, so it surfaces via the uncaught handler.
What memory guarantee does submitting to an executor provide?
- Actions before submit() happen-before the task executes
- All pool threads see each other writes without synchronisation
- The task body is implicitly synchronised on the executor
- Fields written inside a task are automatically volatile
Answer: Actions before submit() happen-before the task executes
It is the same edge as Thread.start(), so handing data into a task is safe. Sharing mutable state between concurrently running tasks still needs synchronisation.
Which API genuinely influences the Linux scheduler on Android?
- Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
- Thread.currentThread().priority = Thread.MIN_PRIORITY
- Thread.currentThread().isDaemon = true
- Thread.yield() inside the loop
Answer: Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
android.os.Process maps to Linux scheduling and cgroups, so background threads get a small CPU share and cannot starve rendering. The java.lang priority is largely advisory here.
Why was AsyncTask deprecated?
- It leaked the Activity, had no lifecycle awareness, and silently serialised parallel work
- It could not perform network calls on modern Android versions
- It was replaced by an identical API living under a completely different package name instead
- It required a Looper, which background threads do not have
Answer: It leaked the Activity, had no lifecycle awareness, and silently serialised parallel work
Its inner-class reference held the Activity, onPostExecute ran against destroyed views, and the default executor became serial in API 11 so parallel work quietly queued.
Why does blocking inside a coroutine on Dispatchers.IO still matter?
- The dispatcher is a real thread pool, so blocking occupies one of its finite threads
- Coroutines forbid blocking calls and throw at runtime
- Blocking calls are relocated onto the main thread automatically by the dispatcher itself
- The coroutine is cancelled once it exceeds a time slice
Answer: The dispatcher is a real thread pool, so blocking occupies one of its finite threads
Coroutines multiplex onto ordinary pools rather than replacing them. Nothing forbids blocking and nothing moves it, which is why the 64-thread ceiling is a genuine limit.
What is the difference between shutdown() and shutdownNow()?
- shutdown() lets queued tasks run; shutdownNow() interrupts running tasks and returns the queued ones
- shutdown() blocks until tasks finish; shutdownNow() returns immediately
- shutdown() affects only new submissions, while shutdownNow() also cancels the already-completed futures
- They are equivalent, differing only in the exception thrown on later submission
Answer: shutdown() lets queued tasks run; shutdownNow() interrupts running tasks and returns the queued ones
Neither blocks, which is what awaitTermination is for. Failing to shut down leaves non-daemon threads alive, which can keep a JVM process running after its work is done.