Concurrency
7 pages · ~5 min✓ Reviewed
Builds on JVM and Memory.
Threads and Runnable Mental Model
1. Thread, Task, Worker
- A thread = independent path of execution
- Shared: the process heap
- Own: stack + program counter
| Role | Result | |
|---|---|---|
| Runnable | describes the task: void run() | nothing |
| Callable<V> | task: V call() | a value; may throw checked |
| Thread | the worker that runs it | - |
2. start() vs run()
start()creates a new thread that callsrun()run()called directly = plain call on the current thread
java
Runnable r = () -> System.out.println(
Thread.currentThread().getName());
Thread t = new Thread(r);
t.start(); t.join();
r.run();output
Thread-0
main3. Thread States
- NEW
- RUNNABLE
- BLOCKEDWAITING, TIMED_WAITING
- TERMINATED
ExecutorService and Thread Pools
1. Submit and Factories
submit(task)→ Future;execute(task)→ nothingget()blocks; task errors wrapped in ExecutionExceptionget(timeout, unit)→ TimeoutException
| Factory | Threads |
|---|---|
newFixedThreadPool(n) | n threads, unbounded queue |
newCachedThreadPool() | unbounded threads |
newSingleThreadExecutor() | one thread |
newVirtualThreadPerTaskExecutor() | virtual thread per task, Java 21 |
2. Shutdown and Core Params
- Under corePoolSizenew thread
- workQueueenqueue
- Queue fullgrow to maximumPoolSize
- Still fullRejectedExecutionHandler
shutdown(): stop accepting, queued tasks finishshutdownNow(): interrupts workers- then
awaitTermination()
java
pool.shutdown(); if (!pool.awaitTermination(30, TimeUnit.SECONDS)) { pool.shutdownNow(); }
Production
Build a ThreadPoolExecutor with a bounded queue and a rejection policy.
3. Interview Point
- Adds threads when? Core first, then queue; beyond core only if queue full
- Avoid newFixedThreadPool? Unbounded queue → OutOfMemoryError
- submit vs execute? submit hides errors in the Future; execute throws
- Fixed vs cached? Fixed caps threads; cached grows threads, no queue
synchronized and volatile
1. synchronized: the monitor
- Mutual exclusion: one thread holds the object's monitor
- Visibility: unlock publishes writes to the next acquirer
- Prefer a
private finallock object overthis
| Form | Locks |
|---|---|
| Instance method | this |
| Static method | the Class object |
| Block | any object you pick |
2. volatile
- Visibility and ordering (happens-before), not atomicity
volatile int count; count++is read, add, write: still a race
| Need | Use |
|---|---|
| Stop flag, immutable reference | volatile |
| Check-then-act, compound update | synchronized |
3. wait / notify
- Call only while holding the monitor, else
IllegalMonitorStateException waitsits in a while loop: spurious wakeups
java
synchronized (lock) {
while (!ready) lock.wait();
}Locks and Atomic Classes
1. ReentrantLock
- Explicit lock() and unlock(); always unlock in
finally - Reentrant: the same thread may re-acquire it
java
lock.lock(); try { count++; } finally { lock.unlock(); }
| synchronized | ReentrantLock | |
|---|---|---|
| Timeout | no | tryLock(timeout) |
| Interrupt | no | lockInterruptibly() |
| Fair order | no | fairness option |
| Wait sets | one | many Conditions |
2. Read-Write Lock and CAS
ReentrantReadWriteLock: many readers or one writer- Good for read-heavy data
- CAS is lock-free:
incrementAndGet(),compareAndSet(expected, new),updateAndGet(fn)
- Readcurrent value
- Comparestill expected?
- Setif equal
- Retryif changed
3. LongAdder and Pitfalls
- LongAdder beats AtomicLong for counters under heavy contention
sum()is not an atomic snapshot- ABA? Value goes A, B, A and fools CAS; use
AtomicStampedReference - Deadlock? Circular lock waiting; take locks in one fixed order
Pick
LongAdder for sum-only counters; AtomicLong when you need exact compareAndSet.
ConcurrentHashMap and CompletableFuture
1. ConcurrentHashMap
- Lock-free reads; writes lock one bin, CAS on empty bins
- No null keys or values
- Use atomic
putIfAbsent,computeIfAbsent,merge - get then put on separate calls = race
java
counts.merge(word, 1, Integer::sum);| Map | Lock | Iterator |
|---|---|---|
| ConcurrentHashMap | per bin | weakly consistent, no CME |
| synchronizedMap | one lock, every call | fail-fast |
| Hashtable | one lock, every call | fail-fast |
2. CompletableFuture
java
CompletableFuture.supplyAsync(() -> x, pool) .thenApply(f) .thenCompose(g) .thenCombine(other, fn) .exceptionally(ex -> dflt);
thenApply= map;thenCompose= flatMap, flattens a returned future- No executor → commonPool; pass your own for blocking IO
allOfwaits for all;anyOffirst to finish
3. Interview Point
- Why no null?
get()returning null is ambiguous: absent key or null value? - thenApply vs thenCompose? Map a value vs flatten a returned future.
- Exceptions?
exceptionally,handleorwhenCompletein the chain. - Why computeIfAbsent? Check and put run as one atomic step.
Concurrency Pitfalls and Cheat Sheet
1. Races and Deadlock
- Race: unsynchronized check-then-act or read-modify-write
- Fix: atomics, locks or concurrent collections
- Deadlock needs circular wait on locks
- Prevent: fixed global lock order, or
tryLockwith timeout
2. Interrupts and Confinement
- Never swallow
InterruptedException: restore the flag or rethrow
java
try { task = queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
- Prefer immutability and thread confinement
ThreadLocalsparingly;remove()in pooled threads or it leaks
3. Cheat Sheet
| Need | Use |
|---|---|
| Stop flag | volatile |
| Compound update | synchronized / atomic |
| Counter | AtomicInteger / LongAdder |
| Shared map | ConcurrentHashMap |
| Async pipeline | CompletableFuture + own executor |
- 4 deadlock conditions? mutual exclusion, hold and wait, no preemption, circular wait
- InterruptedException? restore the flag or rethrow
- Race vs data race? Race: result depends on timing. Data race: unsynchronized access, one a write
Check yourself
Two threads each run count++ 1000 times on a volatile int count. Is the final value always 2000?
- No, it is often less than 2000.
volatilegives visibility, not atomicity:count++is read, add, write.- Fix:
AtomicInteger.incrementAndGet().
A pool has core=2, max=4 and an ArrayBlockingQueue of 10. You submit 5 tasks at once. How many threads run?
- Two. Tasks 3 to 5 wait in the queue.
- A third thread appears only once the queue is full.
On a ConcurrentHashMap: if (!map.containsKey(k)) map.put(k, 1); What is wrong?
- Each call is atomic, but the pair is not: two threads can both see k absent.
- Fix:
map.merge(k, 1, Integer::sum)orputIfAbsent.