Handbooks / Java / Chapter 5

Concurrency

7 pages · ~5 min✓ Reviewed

Builds on JVM and Memory.

JavaPage 1

Threads and Runnable Mental Model

1. Thread, Task, Worker

  • A thread = independent path of execution
  • Shared: the process heap
  • Own: stack + program counter
RoleResult
Runnabledescribes the task: void run()nothing
Callable<V>task: V call()a value; may throw checked
Threadthe worker that runs it-

2. start() vs run()

  • start() creates a new thread that calls run()
  • 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
main

3. Thread States

  1. NEW
  2. RUNNABLE
  3. BLOCKEDWAITING, TIMED_WAITING
  4. TERMINATED
JavaPage 2

ExecutorService and Thread Pools

1. Submit and Factories

  • submit(task) → Future; execute(task) → nothing
  • get() blocks; task errors wrapped in ExecutionException
  • get(timeout, unit) → TimeoutException
FactoryThreads
newFixedThreadPool(n)n threads, unbounded queue
newCachedThreadPool()unbounded threads
newSingleThreadExecutor()one thread
newVirtualThreadPerTaskExecutor()virtual thread per task, Java 21

2. Shutdown and Core Params

  1. Under corePoolSizenew thread
  2. workQueueenqueue
  3. Queue fullgrow to maximumPoolSize
  4. Still fullRejectedExecutionHandler
  • shutdown(): stop accepting, queued tasks finish
  • shutdownNow(): 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
JavaPage 3

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 final lock object over this
FormLocks
Instance methodthis
Static methodthe Class object
Blockany object you pick

2. volatile

  • Visibility and ordering (happens-before), not atomicity
  • volatile int count; count++ is read, add, write: still a race
NeedUse
Stop flag, immutable referencevolatile
Check-then-act, compound updatesynchronized

3. wait / notify

  • Call only while holding the monitor, else IllegalMonitorStateException
  • wait sits in a while loop: spurious wakeups
java
synchronized (lock) {
    while (!ready) lock.wait();
}
JavaPage 4

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();
}
synchronizedReentrantLock
TimeoutnotryLock(timeout)
InterruptnolockInterruptibly()
Fair ordernofairness option
Wait setsonemany 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)
  1. Readcurrent value
  2. Comparestill expected?
  3. Setif equal
  4. 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.

JavaPage 5

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);
MapLockIterator
ConcurrentHashMapper binweakly consistent, no CME
synchronizedMapone lock, every callfail-fast
Hashtableone lock, every callfail-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
  • allOf waits for all; anyOf first 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, handle or whenComplete in the chain.
  • Why computeIfAbsent? Check and put run as one atomic step.
JavaPage 6

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 tryLock with 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
  • ThreadLocal sparingly; remove() in pooled threads or it leaks

3. Cheat Sheet

NeedUse
Stop flagvolatile
Compound updatesynchronized / atomic
CounterAtomicInteger / LongAdder
Shared mapConcurrentHashMap
Async pipelineCompletableFuture + 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
JavaPage 7

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.
  • volatile gives 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) or putIfAbsent.