Skip to main content

Concurrency

Source: Effective Java, 3rd Edition — Chapter 11 by Joshua Bloch (Items 78–81)

Multicore machines require concurrency for good performance. These items cover shared mutable data, synchronization discipline, executors, and java.util.concurrent utilities.

Item 78: Synchronize access to shared mutable data​

synchronized provides mutual exclusion and visibility — without it, one thread’s writes may never be seen by another (memory model).

The broken StopThread pattern: a plain boolean stopRequested with no sync can loop forever due to hoisting. Fix with synchronized read and write methods, or declare the flag volatile when you only need visibility (not compound actions like ++).

volatile int next++ is still broken — ++ is read-modify-write, not atomic. Use synchronized or AtomicLong.getAndIncrement() from java.util.concurrent.atomic.

Best default: don’t share mutable data. Share immutable data, or confine mutation to one thread. Safe publication (final field, volatile, static init, concurrent collection) lets other threads read without further sync once published.

Quiz

Shared mutable data

1 / 2

stopRequested boolean written in main, read in background thread — no sync

Pick an answer to see if you are right.

Item 79: Avoid excessive synchronization​

Never call alien methods (overridable methods, callbacks, lambdas supplied by clients) inside a synchronized block — they can cause ConcurrentModificationException, deadlock, or corrupt invariants.

Open call pattern: copy shared state under the lock, release the lock, then invoke callbacks on the copy:

List<Observer<E>> snapshot;
synchronized (observers) {
snapshot = new ArrayList<>(observers);
}
for (Observer<E> o : snapshot)
o.added(this, element);

For rarely modified observer lists, CopyOnWriteArrayList avoids explicit locking on iteration. Do minimal work while holding locks — oversynchronization hurts throughput on multicore CPUs.

Quiz

Excessive synchronization

1 / 2

notifyElementAdded iterates observers and calls observer.added inside synchronized(observers)

Pick an answer to see if you are right.

Item 80: Prefer executors, tasks, and streams to threads​

Don’t hand-roll worker threads and queues. Use ExecutorService:

ExecutorService exec = Executors.newSingleThreadExecutor();
try {
exec.execute(runnable);
Future<T> f = exec.submit(callable);
} finally {
exec.shutdown();
}

Runnable = no return value; Callable = returns a value. Pick newFixedThreadPool for loaded servers; newCachedThreadPool only for light workloads. Fork/join and parallel streams (Item 48) build on the same ideas.

Item 81: Prefer concurrency utilities to wait and notify​

Use java.util.concurrent instead of hand-coded wait/notify.

Concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue) manage their own locking. Atomic compound ops like putIfAbsent support patterns such as String.intern:

String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null) result = s;
}
return result;

Synchronizers: CountDownLatch, Semaphore, CyclicBarrier, Phaser. If you must maintain legacy wait code: always while (condition) wait(), prefer notifyAll.

Quiz

Concurrency utilities

1 / 2

New code needs threads to wait until N tasks finish setup

Pick an answer to see if you are right.

Items 78–81 map​

ItemMove
78Sync or volatile for visibility; AtomicLong for compound ops
79Open calls — no alien methods inside synchronized
80ExecutorService + Runnable/Callable, not raw Thread
81ConcurrentHashMap, latches — not wait/notify

Next: Thread Safety and Lazy Init (Items 82–84).