Threads, Executors & Thread Pools Flashcards
KOTLIN › JVM Concurrency
- What does a JVM thread actually cost?
- A reserved stack of roughly 512KB to 1MB, kernel scheduling so switching is a real context switch, and a system call to create one (around a millisecond). Those three costs are why pools exist and why coroutines exist.
- What does thread state BLOCKED mean, specifically?
- Waiting to acquire a **monitor lock**, and nothing else. A thread inside a blocking IO call shows as RUNNABLE, which is why thread dumps of IO-heavy apps look busier than they are.
- Why does Callable exist alongside Runnable?
- Because Runnable.run() returns void and cannot declare checked exceptions, so there is nowhere to put a result or a failure. Callable returns a value and may throw, and submitting one gives you a Future.
- What is the fundamental limitation of Future.get()?
- It blocks. There is no completion callback, so composing two async operations means blocking a thread to await the first, which defeats the pool. That limitation drove CompletableFuture, RxJava, and eventually coroutines.
- Why is newCachedThreadPool() dangerous?
- Its queue is a SynchronousQueue with zero capacity, so every task arriving while all threads are busy creates a **new** thread, without limit. On Android that surfaces as OutOfMemoryError: pthread_create failed.
- What is the hidden trap in newFixedThreadPool()?
- Its queue is unbounded, so tasks are never rejected, they accumulate. A producer faster than the consumers grows the queue until the heap is exhausted: it fails quietly on memory rather than loudly on threads.
- In what order does a ThreadPoolExecutor decide what to do with a task?
- Create a thread below core size, then **try to queue**, then create threads up to the maximum only if the queue is full, then reject. Queueing before growing is the step people get wrong.
- Why can maximumPoolSize be dead configuration?
- Because growth past the core size only happens when the queue is full. With an unbounded queue that never occurs, so a pool configured core 2 / max 50 runs exactly two threads and an ever-growing queue.
- How do you size a pool for CPU-bound and IO-bound work?
- CPU-bound: cores, or cores + 1. IO-bound: cores x (1 + wait/compute), so a task waiting 90ms and computing 10ms on 4 cores wants around 40. That reasoning is why Default caps at core count and IO at 64.
- What is the difference between thenApply and thenCompose?
- Exactly map versus flatMap. thenApply transforms the value; thenCompose flattens a future returned by the lambda. Ending up with CompletableFuture<CompletableFuture<T>> means you used the wrong one.
- What happens to an exception thrown by a task submitted with submit()?
- It is stored in the Future and rethrown from get() wrapped in ExecutionException. If nobody calls get() it vanishes silently. execute() has nowhere to store it, so it reaches the thread uncaught handler instead.
- What happens-before guarantee does an executor give you?
- Actions before submit() happen-before the task runs, like Thread.start(). So handing data **into** a task is safe without synchronisation. Sharing mutable data **between** tasks is not.
- Which priority API actually works on Android?
- android.os.Process.setThreadPriority(), which maps to the Linux scheduler and cgroups, so background threads genuinely get a small CPU share. java.lang.Thread.setPriority is largely advisory.
- Do coroutines replace thread pools?
- No, they multiplex onto them. Dispatchers.Default and IO are ordinary pools sized by the standard formulas, so a blocking call inside a coroutine still occupies a real thread from one of them.