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
ConcurrentHashMap achieve thread safety without locking the whole map?In Java 8 and later, get takes no lock and relies on volatile reads. A put into an empty bucket uses compare-and-set, and a put into a non-empty bucket synchronizes only on that bucket's first node. So threads wait only when they write to the same bucket.
ConcurrentHashMap use it?Compare-and-set atomically changes a value only if it still equals the expected one, in a single hardware instruction. The map uses it to install the first node in an empty bucket, to initialise the table, to update its size counters and to coordinate resizing, all without locks.
Lesson: How ConcurrentHashMap works insideConcurrentHashMap, Hashtable and Collections.synchronizedMap.All three are thread-safe for single calls. Hashtable and synchronizedMap use one lock for everything, need manual locking for iteration and can throw a CME, while ConcurrentHashMap locks per bucket, has lock-free reads and weakly consistent iterators. Hashtable and ConcurrentHashMap reject null; synchronizedMap follows the wrapped map.
ConcurrentModificationException?The iterator holds a reference to the array that was current when it was created. Writes create new arrays, so the iterator's array never changes under it. The cost is that the iterator does not see changes made after it started, and iterator.remove() is unsupported.
if (!list.contains(x)) list.add(x) safe on a CopyOnWriteArrayList?No. Each call is atomic, but two threads can both pass contains before either adds, giving duplicates. The list offers addIfAbsent(x), which does the check and the add as one atomic step, and CopyOnWriteArraySet avoids duplicates altogether.
ArrayBlockingQueue and LinkedBlockingQueue?ArrayBlockingQueue is backed by a fixed array, needs a capacity, and uses one lock for both put and take. LinkedBlockingQueue uses linked nodes, has separate put and take locks so both ends can work at once, and is unbounded unless you give a capacity. The array version has fixed memory and no per-item garbage; the linked one often has higher throughput under load.
Executors.newFixedThreadPool carry?It puts waiting tasks in an unbounded LinkedBlockingQueue. If tasks arrive faster than the threads finish them, the queue grows without limit and can exhaust memory. For a busy service I create a ThreadPoolExecutor with a bounded queue and a rejection policy.
I would put requests in a bounded ArrayBlockingQueue or LinkedBlockingQueue in front of a fixed pool of booking workers. Incoming requests use offer with a short timeout, and if the queue is full the user gets a clear "high traffic, please retry" message instead of the server running out of memory.
ConcurrentHashMap<String, List<Order>> thread-safe?The map is, but the lists inside it are not unless I make them so. If several threads call map.get(k).add(order) on an ArrayList, orders can be lost. I would use a thread-safe value such as ConcurrentHashMap.newKeySet() or a synchronized list, or replace immutable lists with compute.
Thread confinement means each thread works only on its own objects, so nothing is shared while the threads run. For jobs like counting words per file, each thread fills its own HashMap and the main thread merges them after join(). It needs no locks at all and is often the simplest and fastest design.
join() waits for a thread to end, so it does not work with thread pools, where threads stay alive. A latch waits for events, not thread deaths: a task can count down halfway through its work, one thread can count down many times, and several threads can wait on the same latch.
Yes, if each worker writes before its countDown() and main reads after await() returns. The Java memory model guarantees that actions before countDown() happen-before actions after a successful return from await(). Writes after countDown() have no such guarantee.
It is a synchronizer where a fixed number of threads wait for each other at a common point; when the last one calls await(), all are released. It is cyclic because it resets itself after each trip, so the same threads can meet again in the next round with the same object.
It means the meeting failed. If one waiting party is interrupted or times out, if reset() is called while threads wait, or if the barrier action throws, the barrier breaks and every other waiting party gets BrokenBarrierException. Later calls to await() also throw it until someone calls reset().
After a broken round, when I want to try again with the same barrier. I call it only when no thread is waiting, because threads that are waiting during reset() get BrokenBarrierException. Often creating a new barrier is simpler and clearer.
It simply adds a permit. A semaphore has no owner and no maximum, so the count can grow above the starting number. That is a bug in a limiter, because more threads than intended can then get in.
Lesson: Semaphore: limit how many at oncenew Semaphore(n, true) gives permits to waiting threads in the order they started waiting. I use it when no caller should be skipped for a long time, for example users waiting for a booking slot. It is a bit slower than the default unfair mode.
It is a reusable synchronizer from java.util.concurrent that runs work in numbered phases. A phase ends when all registered parties have arrived, and unlike a CyclicBarrier, parties can register and deregister at any time, so the group can change between phases.
arrive() marks this party as arrived and returns at once without waiting. arriveAndAwaitAdvance() arrives and waits until the phase ends, like a barrier. arriveAndDeregister() arrives and also removes the party, so later phases no longer wait for it.
When the threads I wait for do not end, such as thread pool threads or a server that signals "ready" and keeps running. A latch waits for events, not for a thread's death, lets several threads wait on the same event and has a timed await. For a few threads I started myself and need finished, join() is simpler.
It gives similar "one at a time" behaviour, but not the same guarantees. A mutex, which means a lock for mutual exclusion, has an owner and Java's locks are reentrant; a Semaphore(1) is neither, so a recursive method that acquires it twice blocks itself. The lack of an owner can be useful, for example when one thread signals another.
It is the flexible one: reusable like a barrier, but parties can register and deregister between phases, and it can work like a latch for a task count found at run time. I use it only when that flexibility is needed, because a latch or barrier is easier for others to read.
Lesson: Choose the right synchronizerWith more parties than threads, the barrier never trips and every thread waits forever, or gets TimeoutException and BrokenBarrierException with a timed wait. With fewer, it trips early and repeatedly, so the barrier action sees partial data, and in the last round some threads are left waiting alone.
I start many threads behind a CountDownLatch(1) start gate so they compete at once, and inside the guarded section I track the current count and a peak with atomics. After all threads finish, the peak must be 3 and never more; holding the section briefly (a short sleep) makes overlap likely.
CompletableFuture?It is a Future from Java 8 that also implements CompletionStage. I can attach steps like thenApply and thenAccept that run when the value is ready, combine several futures, and complete it myself with complete(value).
CompletableFuture chain like a stream?Both are pipelines: each step takes the previous result and passes a new one on, and thenApply plays the role of map. The difference is that a stream processes many values now, while a CompletableFuture processes one value that arrives later.
supplyAsync when you pass no executor?A thread of ForkJoinPool.commonPool(), the shared pool the JVM creates. Its default size is cores minus one, and parallel streams share it. On machines with 1 or 2 cores, CompletableFuture uses a new thread per task instead.
supplyAsync throw a checked exception?No. It must fit Supplier, whose get() declares no checked exceptions. I catch the exception inside the lambda and rethrow an unchecked one like UncheckedIOException; the future then completes with that error.
thenApply, thenAccept and thenRun?thenApply takes a Function and returns a future with a new value. thenAccept takes a Consumer, uses the value and returns CompletableFuture<Void>. thenRun takes a Runnable, does not see the value at all, and also returns CompletableFuture<Void>.
thenApply step?The thread that completes the previous stage, usually a pool thread. If the future is already complete when I attach the step, it runs immediately on my own thread. The documentation does not promise either, so I never rely on it.
Lesson: thenApply, thenAccept and thenRunthenApply is like map, and thenAccept is like forEach at the end of a pipeline. The difference is that a stream works on many values now, while a CompletableFuture works on one value that arrives later.
thenCompose?For dependent async calls, where the next call needs the previous result: find the user, then fetch that user's orders. Each link returns a future, so each link is a thenCompose.
thenCompose?No. Dependent calls must run in order, so thenCompose fits. Independent calls, like fetching a train fare and a hotel price, should start together and be combined with thenCombine or allOf; chaining them would add their times for no reason.
anyOf returns?Nothing stops them; they keep running and their results are ignored. If the work is expensive I can call cancel on the others, but that only marks those futures as cancelled; it does not interrupt the running task.
supplyAsync(a).join(); supplyAsync(b).join(); not parallel?Each join() waits for its task before the next line starts the next task, so the two tasks never overlap. I start both futures first and wait only once, with thenCombine, allOf or two join() calls after both are started.
exceptionally, handle and whenComplete?exceptionally runs only on failure and returns a fallback of the same type. handle always runs with (result, ex) and returns a new value, possibly of another type. whenComplete always runs with (result, ex) but cannot change the outcome, so I use it for logging.
whenComplete itself throws?If the earlier stage succeeded, the future returned by whenComplete completes exceptionally with the action's exception. If the earlier stage had already failed, the original error is kept. Either way, I keep logging code in whenComplete simple so it cannot fail.
The common pool has only about cores minus one threads and is shared by every CompletableFuture and parallel stream in the JVM. Blocking calls hold those threads while they wait, so unrelated async work queues behind them. I give I/O its own executor, sized for the number of calls I want in flight.
orTimeout and completeOnTimeout?Both were added in Java 9. orTimeout fails the future with a TimeoutException when time runs out, so callers must handle the error. completeOnTimeout completes it with a default value instead, which suits optional data like a delivery estimate.
main finishes. What do you check first?Executors that were never shut down. Threads from Executors.newFixedThreadPool are not daemon threads, so they keep the JVM alive. I call shutdown() in a finally block, or use try-with-resources on Java 19 and later.
CompletableFuture never show up in the logs?The task's exception is caught and stored as the future's outcome, so it never reaches the thread's uncaught exception handler. If no one calls join(), get() or a handler on that future, the error is simply lost. I end every chain with a handler that logs it.
I log the unwrapped error with whenComplete at the important stages, and include context such as the order id in exception messages. Naming the executor threads helps too, because log lines and thread dumps then show which pool was running the step.
Mutual exclusion (one holder per lock), hold and wait (a thread keeps its locks while waiting for more), no preemption (locks cannot be taken away) and circular wait (a closed chain of waiting threads). All four must hold together, so breaking any one prevents deadlock.
Lesson: What is a deadlock?Not if it is blocked entering a synchronized block: monitor waiting ignores interrupts, so the thread stays BLOCKED. A thread waiting in Lock.lockInterruptibly() does respond to an interrupt, and tryLock with a timeout gives up by itself. That is one reason the Lock API is used for deadlock-prone code.
I would first look for any stable, unique key, such as an account number or PNR. If there is none, System.identityHashCode gives a number per object, but two objects can tie, so the tie case needs one extra global lock taken before both. Adding a real id is usually cleaner.
transfer method that cannot deadlock.Reject a transfer to the same account. Pick first and second by account number, nest the locks in that order, check the balance and move the money inside. With ReentrantLock, unlock both in finally blocks; with synchronized, the release is automatic.
In a deadlock the threads are blocked, waiting for each other, so they use no CPU. In a livelock the threads are running and keep reacting to each other, for example releasing and retrying locks in step, so the CPU is busy. In both cases no useful work gets done.
Lesson: Livelock and starvationMake the threads behave differently: wait a random time before retrying, ideally a growing wait with jitter, or use a rule that only one side gives way, such as a priority. After that, one thread succeeds and the other follows.
Lesson: Livelock and starvationNot completely. It orders the threads waiting for the lock, but the untimed tryLock() ignores fairness and can jump the queue, and the operating system still decides when each thread runs. A thread that holds the lock for a long time also makes everyone wait, fair or not.
synchronized withdraw and then synchronized deposit. The final balances are correct. Is there a bug?Yes. Between the two calls the money is in neither account, so an audit or a balance screen can see a wrong total. The transfer and every reader of the total must use one lock, or the data must be replaced in one atomic step.
Lesson: Lost updates and broken invariantsbooked + free always equals the capacity?I would store only one of them, booked, and compute free as capacity − booked, so the rule cannot break. If both must be stored, I keep them in one record behind an AtomicReference, or update and read both inside the same synchronized block.
I find the PID with jps -l and run jcmd <pid> Thread.print or jstack <pid>, redirecting the output to a file. On macOS or Linux, kill -3 <pid> makes the JVM print the dump to its own console or log. Tools like JConsole and VisualVM can also show threads and detect deadlocks.
I take several dumps and look for threads that are RUNNABLE in the same method of my own code every time; that is the busy loop or heavy work. Operating system tools that show CPU per thread can be matched to the dump through the native thread id, nid.
The race needs two threads to read and write in an unlucky order. With small loops, the threads hardly overlap, and the JIT compiler can even merge the loop into one addition. So the test passes while the bug is still there; more threads, more work and many rounds make it show up.
Lesson: Test concurrent code honestlyNot proven. Testing can show that a bug exists, never that none exists; the dangerous interleaving may need a different machine or load. I trust the class because of its design, such as one lock per invariant or immutable state, and use the tests to catch mistakes in that design.
Lesson: Test concurrent code honestlyCallable instead of Runnable?When the task must return a result or may throw a checked exception, for example fetching a price from a server. I submit it to an ExecutorService and read the value with Future.get(), which throws ExecutionException if the task failed.
Two or more threads each hold a lock the other needs, so all wait forever. It needs mutual exclusion, hold and wait, no preemption and circular wait. I avoid it mainly with a global lock order, use tryLock with a timeout where the order cannot be controlled, and never call unknown code while holding a lock.
ConcurrentHashMap not allow null keys or values?Because with many threads, a null from get would be ambiguous: is the key missing, or mapped to null? In HashMap you could check with containsKey, but in a concurrent map another thread may change the map between the two calls. Banning null makes get returning null always mean "absent".
synchronized. Can it still have a race?Yes. Callers can combine two synchronized calls into a check-then-act, like if (hasSpace()) add(x), and another thread can act between them. The class should offer one method that does the whole operation under its lock, such as registerIfSpace.
Lost updates from unsafe read-modify-write, check-then-act on shared state, missing volatile on flags, and locks taken in different orders. I prevent them with design: immutable data where possible, one lock per invariant, ready-made classes such as ConcurrentHashMap, atomics and BlockingQueue, lock ordering and open calls, plus stress tests with timeouts.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson