Loading...
Loading...
Java interview guide
Threads, synchronization, locks, executors and concurrent collections. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
notifyAll() wakes threads that cannot do anything. How do you avoid that waste?First I check that it is a real problem: for a few threads the extra wake-ups cost little. If it matters, I use a ReentrantLock with separate Condition objects, such as notFull and notEmpty, so I can signal only the kind of thread that can go on. Ready-made classes such as BlockingQueue already do this.
notifyAll() and not notify() in a bounded buffer?Producers and consumers wait in the same wait set for different conditions. notify() might wake a thread on the same side, which finds its condition false and waits again, so the signal is lost and all threads can end up waiting. notifyAll() always wakes someone who can make progress.
Whether slow work happens inside the synchronized methods, such as making the item, logging or network calls. Only adding to and removing from the queue need the lock; everything else should happen before put or after take, so other threads are not kept waiting.
BlockingQueue?It is a thread-safe queue from java.util.concurrent whose put waits while the queue is full and whose take waits while it is empty. It is a ready-made bounded buffer, so producer-consumer code needs no synchronized, wait or notify of its own.
BlockingQueue not allow null?Because poll and the timed poll use null to say "no item". If null could be stored, a caller could not tell an empty queue from a stored null. So put(null) and offer(null) throw NullPointerException.
volatile running flag not work?When the worker is blocked, for example in queue.take() on an empty queue or in a long sleep. It never returns to the line that checks the flag. Then I need an interrupt, a poison pill, or a timed poll so the loop comes back regularly.
The worker stops at its next blocking call or interrupt check. The item it was handling may be half done, and everything still in the queue stays there. If that work matters, I call drainTo to move the leftovers into a retry list, or I choose a poison pill instead.
An ExecutorService uses the same ideas: shutdown() stops accepting new tasks and lets queued ones finish, like a poison pill, while shutdownNow() interrupts the workers and returns the tasks that never started, like interrupt plus drainTo. The Executors module covers them.
main returns?Because some non-daemon thread is still alive, often a worker blocked in queue.take() or in wait() with nobody left to wake it. The JVM exits only when all non-daemon threads have finished. The fix is a clean stop signal, such as one poison pill per worker or an interrupt.
I would use a bounded ArrayBlockingQueue<String>. App threads call offer with a short timeout, so a slow disk cannot freeze them; if it fails I count a dropped line. The writer loops on take() and writes. At shutdown I stop the app threads, put one poison pill, and join the writer so every queued line is written.
Because a method that holds the lock often calls another method that takes the same lock. A non-reentrant lock would make the thread wait for itself and hang. A reentrant lock keeps a hold count instead and is free only when every lock() has a matching unlock().
ReentrantLock?The lock stays held. A ReentrantLock is not released when its owner ends, and no other thread may call unlock() on it. Any thread that later calls lock() waits forever. A thread dump would show it in the WAITING state.
lock() and tryLock()?lock() waits as long as needed and returns only when it has the lock. tryLock() never waits: it returns true if it took the lock and false if the lock was busy. With tryLock() you must check the result and unlock only on the true path.
A fair lock, new ReentrantLock(true), hands the lock to the thread that has waited longest. It is not the default because it is slower under contention: every hand-over must wake a parked thread, while an unfair lock lets a running thread take a free lock at once. The default trades some fairness for speed.
tryLock and lockInterruptibly throw InterruptedException but tryLock() does not?The first two can wait, and any method that waits should let the waiting thread be cancelled. Plain tryLock() returns at once, so there is no waiting to interrupt. The checked exception forces the caller to decide what cancellation means.
while loop around await()?Yes. A thread can wake without the condition being true: a spurious wakeup, or another thread took the item first. The loop re-checks the condition after every wake-up, exactly as with wait().
notEmpty.wait() throw IllegalMonitorStateException even though you hold the lock?wait() is the Object method that needs the object's built-in monitor, which only synchronized (notEmpty) would give. Holding the ReentrantLock is a different lock. The Condition method is await().
synchronized. It is shorter, it cannot leak a lock on an exception, and its owner shows clearly in thread dumps. I switch to ReentrantLock only when a requirement needs a timeout, cancellation, fairness or more than one condition.
ReentrantLock.A ticket booking service where a request must not hang if a seat is being edited. I would use tryLock(2, TimeUnit.SECONDS) and return "Please try again" on false. With synchronized the request thread could wait without any limit.
ReadWriteLock?It is a pair of locks for the same data: a shared read lock and an exclusive write lock. Many threads can read together, but a writer needs the data to itself, with no readers and no other writer. ReentrantReadWriteLock is the standard implementation.
Yes. A thread holding the write lock may take the read lock and then release the write lock. It keeps reading with no gap in which another writer could change the data, while other readers are allowed back in.
Lesson: ReadWriteLock for read-heavy dataReentrantReadWriteLock for a HashMap cache today?Often ConcurrentHashMap is simpler and faster: it allows concurrent reads and writes on different keys without any lock in your code. A read-write lock is still useful when one operation must read or change several values together, or when the protected data is not a map.
incrementAndGet() and getAndIncrement()?Both add one atomically. incrementAndGet() returns the new value, like ++x; getAndIncrement() returns the value before the change, like x++. For a token machine starting at 0, incrementAndGet() hands out 1, 2, 3 and getAndIncrement() hands out 0, 1, 2.
updateAndGet be free of side effects?Because the method may call it more than once. If another thread changes the value between reading it and storing the result, the update is retried with the fresh value. Printing, sending messages or changing other variables inside the lambda would then happen several times.
Lesson: AtomicInteger and AtomicLongAn atomic operation that sets a variable to a new value only if it still holds an expected value, and returns whether it worked. On most CPUs it is a single instruction, so it needs no lock. Java exposes it as compareAndSet on the atomic classes.
A thread reads value A, others change it to B and back to A, and the thread's CAS succeeds as if nothing had happened. It matters when the same value can mean a different state, such as a reused node. AtomicStampedReference solves it by pairing the value with a version stamp that changes on every update, and its CAS checks both.
It is thrown away and done again with the new value. So the computation inside must be cheap and free of side effects. Anything like logging, sending a message or adding to a list must happen after the loop has succeeded.
Lesson: Compare-and-set: how atomics workLongAdder.sum() accurate?It is exact when no thread is updating, for example after all workers have finished. While updates are running it is not an atomic snapshot: it reads the cells one by one, so it may include some updates in progress and miss others. For dashboards and statistics that is acceptable.
Lesson: LongAdder for busy counterssumThenReset(). Can a request be lost or double-counted?It cannot be counted twice, and it is not lost from the overall total, but a request that arrives during the reset may be counted in this minute or the next. If exact per-period numbers matter while traffic continues, use a different design, such as swapping in a fresh counter or a lock around the period switch.
Lesson: LongAdder for busy countersLongAdder?By the rule I need to protect. For one busy counter that is read rarely, LongAdder. For one value where callers need the result or a limit, an AtomicInteger or AtomicLong with incrementAndGet or getAndUpdate. For several values that must change together, or longer work, a lock, synchronized by default.
tryLock with a timeout in a real application?When waiting forever is worse than failing, for example a booking request that should answer "please try again" instead of freezing, or code that needs two locks and should back off rather than risk waiting forever. I always check its result and unlock only when it returned true.
Each platform thread maps to an operating system thread with its own stack, so creating and destroying it costs time and memory. With one thread per task there is also no limit: a traffic spike can create thousands of threads, slow everything down with context switches, and end in OutOfMemoryError: unable to create native thread.
Five tasks start running on the five threads, and the other 95 wait in the queue. Whenever a thread finishes a task, it takes the next one from the queue, until the queue is empty.
Lesson: Why use a thread pool?submit wraps the task, catches the exception and stores it in the Future. It only comes out when someone calls get(), wrapped in an ExecutionException. The fix is to call get() and handle the exception, or to catch and log exceptions inside the task.
newSingleThreadExecutor() the same as newFixedThreadPool(1)?They behave the same: one thread, tasks in order, a dead thread is replaced. The difference is that newFixedThreadPool(1) returns a ThreadPoolExecutor you could cast and resize later, while newSingleThreadExecutor() is wrapped so it is guaranteed to stay single-threaded.
Runnable and Callable?Runnable.run() returns nothing and cannot throw checked exceptions. Callable.call() returns a value and can throw any exception. You can pass a Runnable to new Thread, execute or submit; a Callable goes to submit, invokeAll or invokeAny, and its result comes back through a Future.
get()?Use get(timeout, unit). If it throws TimeoutException, I decide what to do: show a fallback, retry later, or call cancel(true) so the task does not keep using a thread for nothing.
get() right after each submit. What is wrong?Each get() waits for that task before the next one is even submitted, so the tasks run one after another and the pool gives no speed-up. I submit all tasks first, keep the futures in a list, and then call get() on each.
invokeAll batch throws. Do you lose the other results?No. Each task has its own future. Only that task's get() throws ExecutionException; the others still return their values, as long as I wrap each get() in its own try.
invokeAll, even though you need all results?When I want to act on each result as soon as it arrives, or keep the calling thread busy with other work. invokeAll blocks until the slowest task is done, so I would submit the tasks myself, or use CompletableFuture, which is covered in its own module.
One run threw an exception. For repeating tasks that cancels all later runs, and the exception is only stored in the ScheduledFuture, so nothing is logged. I would wrap the task body in try/catch with logging, and alert if the future ever becomes done without being cancelled.
Thread.sleep in a loop instead of a scheduler?For a tiny program, yes, but it ties up a whole thread per loop, drifts because the loop body time adds to each gap, and needs hand-written stop and error handling. A scheduler shares a few threads among many timed tasks and gives you cancel and shutdown for free.
Lesson: Run tasks later and repeatedlyshutdownNow() guarantee that running tasks stop?No. It only interrupts them. A task stops if it reacts to interruption, through sleep, wait, a blocking queue or an explicit isInterrupted() check. A tight loop that never checks keeps running, and awaitTermination will time out.
shutdown()?It is rejected: by default submit or execute throws RejectedExecutionException. That usually points to a shutdown that happens too early, for example while another thread is still producing work.
Each core runs one thread at a time. Once every core is busy, extra threads only take turns, and every context switch costs time and cache warmth. So beyond the core count you get no speed-up and often a small slowdown.
Lesson: Choose a pool size and configure ThreadPoolExecutorCallerRunsPolicy?AbortPolicy throws, CallerRunsPolicy runs the task on the submitting thread, DiscardPolicy drops it and DiscardOldestPolicy drops the oldest queued task. I use CallerRunsPolicy when no task may be lost and I want back-pressure: the producer slows down because it is busy running the overflow itself.
A platform thread wraps an operating system thread, with a large fixed stack, so it is expensive and we keep few of them. A virtual thread is managed by the JVM, keeps its stack on the heap, and runs on a small set of carrier threads. When it blocks it unmounts and frees the carrier, so we can afford one virtual thread per task, even millions.
Lesson: Virtual threads (Java 21)Pinning is when a virtual thread cannot unmount from its carrier while it blocks. On Java 21 that happens when it blocks inside synchronized or native code, so the carrier is wasted for the whole wait. I avoid it by using ReentrantLock around code that blocks, and I can find it with -Djdk.tracePinnedThreads=full. Java 24 removed pinning for synchronized.
Yes, they are always daemon threads, so they do not keep the JVM alive by themselves. That is why the program must wait for them, with join() or by closing the executor in a try-with-resources block, before main ends.
Every request pays the cost of creating threads, and if the pool is not shut down its idle threads stay alive for ever, so the thread count grows with traffic until the server fails. One shared, correctly sized pool reuses threads across all requests.
Lesson: Executors: common mistakes and practiceA class that creates one pool in its constructor and implements AutoCloseable with the two-step shutdown. Its batch method submits all tasks first, waits with one overall deadline, and turns each future into a result: a value, the real cause of an ExecutionException, or a timeout followed by cancel(true).
put on one HashMap?Entries can be lost, size() can be wrong, and the inner links can be damaged, so a later lookup misses a key that was stored. In Java 7 and earlier a concurrent resize could link a bucket in a circle and make get loop forever. Java 8 fixed that specific loop, but the map is still not safe to share.
Vector and ArrayList?Both are resizable arrays with the same List methods, but every public method of Vector is synchronized and ArrayList has no locking at all. That makes ArrayList faster for one thread. Vector is a legacy Java 1.0 class and is rarely chosen in new code.
Collections.synchronizedList make a list thread-safe?It returns a wrapper object that holds the real list and runs each method inside synchronized (mutex), where the mutex is the wrapper itself. So only one thread at a time can be inside any method of that wrapper.
Either hold synchronized (list) for the whole loop, as the Java docs require, or take a snapshot with new ArrayList<>(list) and loop over the copy. The snapshot keeps the lock short, which is better when the loop body is slow.
When many threads use the collection heavily, because every read and write waits for the same lock. For a busy shared map I would use ConcurrentHashMap, which lets many threads work at once, and for a list that is read far more than written, CopyOnWriteArrayList.
ConcurrentModificationException from a ConcurrentHashMap?Not from its iterators. They are weakly consistent: they never throw CME, they visit each entry present at the start once, and they may or may not show changes made during the loop. So removing entries inside a for-each loop over the map is fine.
Lesson: ConcurrentHashMap basicsConcurrentHashMap method is thread-safe, is any code that uses it thread-safe?No. Two calls in a row, like get then put, or size() then put, are separate steps that other threads can interrupt. I either use one atomic method such as merge, compute or putIfAbsent, or guard the extra logic with another tool like an atomic counter.
putIfAbsent and computeIfAbsent?putIfAbsent takes a ready value, so that value is created even when the key already exists. computeIfAbsent takes a function and runs it only when the key is missing, at most once per key in a ConcurrentHashMap. For anything expensive, like a database load or a new list, I use computeIfAbsent.
compute over merge?When the new value is not just a combination of old and new, or when the logic needs the key, such as keeping the maximum mark, updating a field in a record, or removing the entry under some condition. compute gives me the key and the old value (possibly null) and stores whatever I return.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson