Chapter 10 Flashcards — Concurrency
flashcards effective-java concurrency threading
What two guarantees does synchronization provide in Java?
?
- Mutual exclusion: only one thread executes a synchronized block at a time, preventing partial reads or writes of compound operations. 2. Visibility: a thread that exits a synchronized block is guaranteed to flush its changes to main memory; a thread that enters a synchronized block sees all changes made before the corresponding unlock. Both guarantees are needed — synchronizing only the write is insufficient.
What does volatile guarantee, and what does it NOT guarantee?
?
volatile guarantees visibility: a read of a volatile field always sees the most recent write from any thread. It does NOT guarantee atomicity. volatile int count; count++ is still a non-atomic read-modify-write (three steps: read, add, write). Use volatile for simple flags and references. Use AtomicInteger / synchronized for compound operations.
Show a classic race condition where volatile alone is insufficient.
?
private static volatile int count = 0;
public static void increment() {
count++; // NOT atomic: read value, add 1, write — threads can interleave
}Two threads both read count = 0, both compute 1, both write 1. Final value is 1, not 2. Fix: use AtomicInteger.incrementAndGet() or synchronized.
What is the Java Memory Model’s happens-before relationship?
?
If action A happens-before action B, all side effects of A are visible to B. Key rules: (1) Program order within a thread. (2) Monitor unlock happens-before subsequent lock on the same monitor. (3) Write to volatile field happens-before subsequent read of that field. (4) Thread.start() happens-before any action in the started thread. (5) All thread actions happen-before Thread.join() returns. Without a happens-before chain, a thread may see stale data.
What is the broken double-checked locking pattern, and how do you fix it?
?
Broken (no volatile):
private static Singleton instance; // MISSING volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton(); // ref may be visible before constructor finishes!
}
}
return instance;
}Fix: declare instance as private static volatile Singleton instance;. The volatile write happens-before any subsequent read, ensuring the fully-constructed object is always seen.
What is the initialization-on-demand holder idiom?
?
A lazy singleton pattern that uses a private static nested class as the holder. The holder class is loaded — and the instance initialized — only when getInstance() is first called. Thread safety comes from the JVM class-loading guarantee (class initialization is single-threaded). Zero synchronization overhead after first access.
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() { return Holder.INSTANCE; }Preferred over double-checked locking for static fields.
What is the difference between CountDownLatch and CyclicBarrier?
?
CountDownLatch: one-shot. Initialized with count N. Threads call countDown() to decrement; other threads block on await() until count reaches 0. Cannot be reset. Use for “wait until N events happen.” CyclicBarrier: reusable. All N parties must call await() before any proceed. Optional barrier action runs on last arrival. Resets automatically. Use for “synchronize N threads at a phase boundary.” Key: CountDownLatch counts external events; CyclicBarrier synchronizes a fixed set of threads.
What does CopyOnWriteArrayList do, and when should you use it?
?
CopyOnWriteArrayList makes a fresh copy of the underlying array on every write (add, set, remove). Readers iterate a snapshot and never block. Use when: reads vastly outnumber writes (e.g., observer/listener lists). Don’t use when: writes are frequent — each write copies the entire array. Eliminates the need to hold a lock during iteration, preventing alien-method-caused deadlocks in observer patterns.
What is wrong with calling an alien method while holding a lock?
?
An alien method (overridable or external) may acquire other locks in an uncontrolled order, creating a potential deadlock cycle. While holding your lock, the alien method may try to acquire a second lock; another thread may hold that second lock and be waiting for your lock. Neither thread can proceed. Fix: copy the needed state under the lock, release the lock, then call the alien method outside the lock.
What is the difference between synchronized and ReentrantLock?
?
Both provide mutual exclusion and visibility. ReentrantLock additionally offers: (1) timed lock acquisition (tryLock(timeout)), (2) interruptible lock acquisition, (3) fairness policy, (4) multiple condition variables per lock (newCondition()), (5) no carrier-thread pinning with virtual threads. synchronized is simpler (no try/finally needed) and sufficient for most cases. Prefer ReentrantLock when you need features (1)-(4) or when using virtual threads (Java 21+).
What is StampedLock, and when should you use it?
?
StampedLock (Java 8+) is a lock with three modes: write lock, read lock, and optimistic read (non-blocking). Optimistic read returns a stamp; after reading, validate the stamp to check for concurrent writes — if validation fails, fall back to a full read lock. Significantly outperforms ReentrantReadWriteLock for read-dominant, write-rare workloads. Caveats: non-reentrant, no condition variables, more complex API. Use only in performance-critical code where profiling shows contention on RRWL.
What are virtual threads, and how do they change the concurrency model?
?
Virtual threads (Java 21, JEP 444) are lightweight threads managed by the JDK, multiplexed onto a small pool of OS (carrier) threads. Creating millions of virtual threads is cheap (few hundred bytes vs. ~1MB for platform threads). Blocking a virtual thread (I/O, sleep, lock) releases the carrier thread for other virtual threads. Impact: I/O-bound workloads no longer need async/reactive programming — simple blocking code with virtual threads achieves the same scalability. Use Executors.newVirtualThreadPerTaskExecutor().
What is the “pinning” problem with virtual threads and synchronized?
?
If a virtual thread enters a synchronized block and then blocks (I/O, sleep), the carrier (platform) thread is pinned — it cannot run other virtual threads until the virtual thread exits the synchronized block. This defeats the purpose of virtual threads for blocking I/O. Fix: replace synchronized blocks with ReentrantLock in code that will run on virtual threads and contains blocking operations.
Why should you prefer executors over raw Thread creation?
?
Raw new Thread() gives you: no lifecycle management, no work queue, no resource bounding, no retry, no monitoring. Under load, unbounded thread creation can crash the JVM with OutOfMemoryError. Executors provide: bounded thread pools, work queues that buffer tasks under load, configurable rejection policies, monitoring via ThreadPoolExecutor metrics, and correct thread reuse. Always use ExecutorService or CompletableFuture in production code.
What are the four executor types and their use cases?
?
newSingleThreadExecutor(): sequential background processing, guaranteed ordering. 2.newFixedThreadPool(n): bounded concurrency; usen = availableProcessors()for CPU-bound work. 3.newCachedThreadPool(): unbounded, reuses idle threads; good for I/O-bound short tasks (watch for thread explosion). 4.newScheduledThreadPool(n): periodic/delayed task execution. Java 21+:newVirtualThreadPerTaskExecutor()for I/O-bound work.
What did Java 9 add to CompletableFuture?
?
orTimeout(long timeout, TimeUnit unit): completes the future exceptionally with TimeoutException if not done within the timeout. completeOnTimeout(T value, long timeout, TimeUnit unit): completes the future with a default value if not done within the timeout. These eliminate boilerplate for timeout handling in async pipelines.
When should you use parallel streams vs. a custom ForkJoinPool?
?
Parallel streams use the common ForkJoinPool (sized to CPU count). Use for CPU-bound, independent, side-effect-free operations. Custom ForkJoinPool: when you need a different parallelism level or to isolate parallel work from the common pool. Pass the stream computation as a task: customPool.submit(() -> stream.parallel()...count()).get(). Never use parallel streams for I/O-bound work — the common pool is sized for CPU, not for blocking.
What is the correct pattern for waiting in a loop using wait/notify?
?
synchronized (obj) {
while (!condition) // ALWAYS loop — handles spurious wakeups
obj.wait();
// condition is now true
}Always loop on wait() — spurious wakeups (wakeup with no corresponding notify) are permitted by the JMM. Use notifyAll() over notify() — notify() wakes one arbitrary thread, which may not be the right one, causing progress to stall.
What is ConcurrentHashMap.computeIfAbsent, and why is it preferred over check-then-put?
?
computeIfAbsent(key, mappingFunction) atomically computes and inserts a value if the key is absent. The entire check-compute-insert is a single atomic operation. Manual check-then-put has a race: two threads can both see a missing key, both compute values, and one overwrites the other. computeIfAbsent is the correct tool for “get or create” patterns on ConcurrentHashMap.
What is the single-check idiom for lazy initialization?
?
private volatile FieldType field;
public FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeField(); // no lock — may run multiple times
return result;
}Used when: (1) repeated initialization is tolerable (idempotent), (2) performance of double-checked locking isn’t needed. volatile ensures visibility. Multiple threads may compute and write the value, but all compute the same result. Simpler than double-checked locking; use only when reinitializing is harmless.
What are thread safety levels, from safest to least safe?
?
- Immutable: no mutable state; safe without any synchronization (
String,Long). 2. Unconditionally thread-safe: mutable but internally synchronized; safe to call from any thread (ConcurrentHashMap,AtomicLong). 3. Conditionally thread-safe: thread-safe if specific client-side conditions are met (e.g.,Collections.synchronizedList— iteration requires external sync). 4. Not thread-safe: clients must synchronize (ArrayList,HashMap). 5. Thread-hostile: unsafe even with external sync (modifies static state without sync).
What is the @GuardedBy annotation, and how should you use it?
?
@GuardedBy("lockName") (from JCIP annotations or JSR-305) documents that a field or method must be accessed only while holding the specified lock. Example: @GuardedBy("this") private int count; means count must only be read or written while holding the object’s intrinsic lock. Static analysis tools (SpotBugs, IntelliJ) can verify this. Use it alongside @ThreadSafe to make thread safety contracts explicit and machine-checkable.
Why is a private lock object better than synchronizing on ‘this’ in a public class?
?
Synchronizing on this exposes the lock to external code — any client can synchronized(yourObject) and hold your lock, potentially causing deadlocks or interfering with your internal synchronization. A private lock object (private final Object lock = new Object()) is inaccessible to external code, giving you full control. Also enables switching from intrinsic locks to ReentrantLock without API changes.
What is structured concurrency (Java 21)?
?
StructuredTaskScope (JEP 453) enforces that a group of concurrent tasks forms a tree with a defined lifetime: all subtasks finish before the scope exits. Two built-in policies: ShutdownOnFailure (cancel remaining tasks if any fail; propagate first failure) and ShutdownOnSuccess (cancel remaining tasks when first succeeds; return first result). Prevents resource leaks and makes concurrent code easier to reason about than CompletableFuture composition.
What is the canonical fix for a busy-wait loop?
?
Replace the busy-wait with a blocking primitive. Busy-wait (while (!condition) {}) wastes CPU and starves other threads. Instead: use BlockingQueue.take() for producer-consumer, CountDownLatch.await() for one-shot events, CyclicBarrier.await() for phase synchronization, or a Condition.await() for arbitrary condition waiting. All of these efficiently park the thread until the condition is met.
What is VarHandle (Java 9+), and how does it differ from AtomicInteger?
?
VarHandle provides fine-grained atomic access to variables: getAndAdd, compareAndSet, getAndSet, and memory fence operations (fullFence, acquireFence, releaseFence). It is the official replacement for sun.misc.Unsafe in library code. Unlike AtomicInteger (a separate wrapper object), VarHandle operates directly on a field of your class, avoiding object allocation overhead. Application code: use AtomicInteger/AtomicReference. Library/framework code: use VarHandle for performance-critical atomic field access.
When would you use a Semaphore?
?
Semaphore limits concurrent access to a bounded resource pool. Initialized with N permits; acquire() blocks until a permit is available; release() returns a permit. Use cases: (1) limiting concurrent database connections, (2) rate limiting, (3) controlling access to a fixed pool of resources. Not a general mutex — callers that acquire() do not need to be the same as callers that release() (unlike ReentrantLock).
What are scoped values (Java 21+), and when do they replace ThreadLocal?
?
ScopedValue (JEP 446) is a mechanism for passing immutable, contextual data to a bounded scope of code, including across virtual thread boundaries. Unlike ThreadLocal, scoped values: are immutable within a scope, are automatically unset when the scope exits (no remove() needed), and do not “inherit” across virtual threads by default (preventing memory leaks). Use for passing request context (user ID, trace ID) through call stacks, especially in virtual-thread-heavy code.
What is the rule for the number of runnable threads in a correct concurrent program?
?
The number of runnable threads (not blocked, not sleeping — actually consuming CPU) should not significantly exceed the number of available processors. Excess runnable threads cause context switching overhead and make scheduling dependent. Keep threads doing real work (CPU-bound) equal to processor count; I/O-bound threads are fine in larger numbers because they spend most time blocked. Virtual threads make this rule less critical — the JDK scheduler manages the mapping.
What should you do when catching InterruptedException?
?
Either: (1) Propagate it — re-declare throws InterruptedException in your method signature (preferred in most cases). Or: (2) Restore the interrupt flag — call Thread.currentThread().interrupt() after catching, then handle the interruption (return early, set a stop flag, etc.). Never silently swallow InterruptedException — catching it clears the thread’s interrupt status, preventing higher-level code from detecting the interrupt signal.
What is the difference between notify() and notifyAll()?
?
Both wake threads waiting on an object’s monitor. notify() wakes one arbitrary waiting thread. If the wrong thread is woken (one whose condition is not yet true), it will wait() again, but the thread that was supposed to wake up is missed — progress stalls. notifyAll() wakes all waiting threads; each checks its condition; the correct one proceeds, others return to waiting. Always prefer notifyAll() unless you have a specific reason to use notify() (identical conditions, homogeneous waiters) and understand the implications.
Total Cards: 31
Review Time: ~40 minutes
Priority: HIGH
Last Updated: 2026-05-10