The Concurrency Utilities

High-level tools for concurrent programs: synchronizers, executors, callable tasks, locks, atomics, and the Fork/Join Framework

What this chapter is about. Java's original threading model (Thread, Runnable, synchronized, wait()/notify()) is elegant but low-level. JDK 5 added the concurrency utilities (the "concurrent API") in java.util.concurrent and its subpackages, giving you ready-made semaphores, latches, barriers, thread pools, executors, locks, atomic variables, and concurrent collections. JDK 7 added the Fork/Join Framework for true parallel (multicore) execution, enhanced in JDK 8.

1. The Big Idea

Many Java programs use threads and are therefore "concurrent" in a loose sense. This chapter uses concurrent program to mean one that makes extensive, integral use of concurrently executing threads — for example, several threads computing partial results of one large computation, or coordinating access to a database. For such programs, the built-in primitives are not enough; you want higher-level building blocks that are correct, reusable, and tuned for a specific job.

Do the concurrency utilities replace the traditional approach? No. synchronized, wait(), and notify() remain the right tool for many, many programs. Reach for the concurrent API when you need extra control, and for the Fork/Join Framework when you want parallel execution.

2. Why This Topic Matters

  • Hand-crafting a correct semaphore, latch, or barrier is difficult and error-prone; the concurrent API standardises them.
  • Thread pools / executors avoid the overhead of creating a thread per task and are applicable even to lightly concurrent programs.
  • Callable / Future lets a thread return a value — ideal for parallel numeric computation.
  • Multicore machines are the norm, so Fork/Join (divide-and-conquer parallelism that auto-scales to available processors) is increasingly common.

3. The Concurrent API Packages

PackageContents
java.util.concurrentCore features: synchronizers, executors, concurrent collections, the Fork/Join Framework, the TimeUnit enumeration, and (JDK 9+) the Flow reactive-streams subsystem.
java.util.concurrent.atomicLock-free updates of single variables via classes like AtomicInteger, AtomicLong and methods like compareAndSet(), getAndSet(), decrementAndGet().
java.util.concurrent.locksAn alternative to synchronized: the Lock interface (lock(), tryLock(), unlock()) with more control.

Since JDK 9 all are part of the java.base module.

4. Synchronizers

Each synchronizer solves one specific synchronization problem, so each can be optimised for its use.

ClassPurpose
SemaphoreClassic counting semaphore: controls access to a resource through a count of permits.
CountDownLatchWaits until a specified number of events have occurred.
CyclicBarrierLets a group of threads wait at a predefined point until all have reached it; reusable.
ExchangerExchanges data between exactly two threads.
PhaserSynchronizes threads advancing through multiple phases of an operation.

4.1 Semaphore

A semaphore's counter counts permits. To use a resource a thread calls acquire() (blocks if the count is zero, otherwise decrements it); when finished it calls release() (increments it, possibly waking a waiter).

Semaphore(int num) // num = initial permit count Semaphore(int num, boolean how) // how = true -> FIFO fairness void acquire() throws InterruptedException void acquire(int num) throws InterruptedException void release() void release(int num)
// A simple semaphore example (condensed). import java.util.concurrent.*; class Shared { static int count = 0; } class IncThread implements Runnable { String name; Semaphore sem; IncThread(Semaphore s, String n) { sem = s; name = n; } public void run() { try { sem.acquire(); // get a permit for(int i = 0; i < 5; i++) { Shared.count++; System.out.println(name + ": " + Shared.count); Thread.sleep(10); // invite a context switch } } catch (InterruptedException exc) { System.out.println(exc); } sem.release(); // release the permit } } // DecThread is identical but does Shared.count-- class SemDemo { public static void main(String[] args) { Semaphore sem = new Semaphore(1); // only 1 thread at a time new Thread(new IncThread(sem, "A")).start(); new Thread(new DecThread(sem, "B")).start(); } }

What it shows: although sleep() would normally let the other thread run, the semaphore forces A to finish all five increments before B gets the permit — so the increments and decrements are not intermixed. Comment out acquire()/release() and the output becomes interleaved.

Powerful extra: you can set the initial state. The producer/consumer rework uses semProd = new Semaphore(1) and semCon = new Semaphore(0) so that put() always runs before get(), and each put() is matched by exactly one get().

4.2 CountDownLatch

CountDownLatch(int num) // num events must occur void await() throws InterruptedException boolean await(long wait, TimeUnit tu) throws InterruptedException void countDown() // decrements the count
CountDownLatch cdl = new CountDownLatch(5); new Thread(new MyThread(cdl)).start(); // calls cdl.countDown() 5 times cdl.await(); // main thread blocks until count == 0 System.out.println("Done");

When the count reaches zero the latch opens and every waiting thread proceeds. Use it whenever a thread must wait for one or more events to finish.

4.3 CyclicBarrier

CyclicBarrier(int numThreads) CyclicBarrier(int numThreads, Runnable action) // action runs when the barrier trips int await() throws InterruptedException, BrokenBarrierException
CyclicBarrier cb = new CyclicBarrier(3, new BarAction()); new Thread(new MyThread(cb, "A")).start(); new Thread(new MyThread(cb, "B")).start(); new Thread(new MyThread(cb, "C")).start(); // each MyThread does its work, then calls cb.await(); // when the 3rd arrives, BarAction runs, then all resume.

await() returns the arrival order (last thread returns 0, first returns numThreads - 1). A CyclicBarrier is reusable: it trips again every time numThreads more threads call await().

4.4 Exchanger

Exchanger<V> // V = type of data exchanged V exchange(V objRef) throws InterruptedException

exchange() blocks until two threads have called it on the same Exchanger, then each receives the other's object. Classic use: one thread fills a buffer while the other drains it, swapping "full" for "empty" each round.

Exchanger<String> exgr = new Exchanger<String>(); // MakeString: str = ex.exchange(str); // give full, get empty // UseString: str = ex.exchange(new String()); // give empty, get full -> "ABCDE", "FGHIJ", ...

4.5 Phaser

Like a CyclicBarrier but for multiple phases. "Parties" register with the phaser; each phase completes when all registered parties arrive, then the phaser advances.

Phaser() // registration count 0 Phaser(int numParties) // start with numParties registered; current phase = 0 int register() // register a party after construction int arrive() // signal arrival, DO NOT wait int arriveAndAwaitAdvance() // signal arrival AND wait for the phase to complete int arriveAndDeregister() // signal arrival and deregister final int getPhase() // current phase number (negative if terminated)
Phaser phsr = new Phaser(1); // 1 = the main thread new Thread(new MyThread(phsr, "A")).start(); // each MyThread calls phsr.register() // ... B, C ... curPhase = phsr.getPhase(); phsr.arriveAndAwaitAdvance(); // wait for phase 0 System.out.println("Phase " + curPhase + " Complete"); // repeat for phases 1 and 2, then: phsr.arriveAndDeregister(); // no parties left -> phaser terminates

Controlling phase transitions: override protected boolean onAdvance(int phase, int numParties). Return true to terminate the phaser, false to keep it alive. The default terminates when there are no registered parties. Overriding it (via a subclass or an anonymous inner class) lets a phaser run exactly N phases and stop. Other methods: awaitAdvance(int phase), bulkRegister(), getRegisteredParties(), forceTermination(), and phaser trees via a parent.

Remember (synchronizers): each one targets one problem — count of permits (Semaphore), count of events (CountDownLatch), a meeting point (CyclicBarrier), a two-party swap (Exchanger), multi-phase coordination (Phaser).

5. Executors

An executor initiates and controls thread execution — an alternative to managing Thread objects yourself.

TypeRole
ExecutorBase interface: void execute(Runnable thread) starts a thread.
ExecutorServiceExtends Executor; adds shutdown(), methods that run value-returning tasks, run sets of tasks, and report status.
ScheduledExecutorServiceExtends ExecutorService to support scheduling.
ImplementationsThreadPoolExecutor, ScheduledThreadPoolExecutor, ForkJoinPool.

A thread pool reuses a fixed set of threads across many tasks, avoiding per-task thread creation cost. Usually you obtain one from the Executors factory:

static ExecutorService newCachedThreadPool() // grows/reuses as needed static ExecutorService newFixedThreadPool(int numThreads) // fixed size static ScheduledExecutorService newScheduledThreadPool(int numThreads)
ExecutorService es = Executors.newFixedThreadPool(2); // 2 threads es.execute(new MyThread(cdl, "A")); es.execute(new MyThread(cdl2, "B")); es.execute(new MyThread(cdl3, "C")); es.execute(new MyThread(cdl4, "D")); // 4 tasks share 2 threads // ... await the latches ... es.shutdown(); // REQUIRED or the program will not exit
Without shutdown() the executor stays active and the JVM does not terminate.

6. Callable and Future

Callable<V> is like Runnable but its single method returns a value (or throws):

interface Callable<V> { V call() throws Exception; } // Submit to an ExecutorService: <T> Future<T> submit(Callable<T> task) // Future<V> holds the "future" result: V get() throws InterruptedException, ExecutionException V get(long wait, TimeUnit tu) throws ..., TimeoutException
ExecutorService es = Executors.newFixedThreadPool(3); Future<Integer> f = es.submit(new Sum(10)); // Callable<Integer> Future<Double> f2 = es.submit(new Hypot(3, 4)); // Callable<Double> Future<Integer> f3 = es.submit(new Factorial(5)); System.out.println(f.get()); // 55 (blocks until ready) System.out.println(f2.get()); // 5.0 System.out.println(f3.get()); // 120 es.shutdown();

All three computations run simultaneously; each get() waits for its result.

7. The TimeUnit Enumeration

Many concurrent-API methods take a TimeUnit to express a timeout's granularity: DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS (the system may not actually support the finest resolution).

f.get(10, TimeUnit.MILLISECONDS); // wait at most 10 ms, else TimeoutException

It also provides conversions (convert(), toMillis(), toNanos(), …; JDK 9 added toChronoUnit()/of(), JDK 11 a Duration overload of convert()) and timing helpers sleep(), timedJoin(), timedWait().

8. Concurrent Collections

Concurrency-engineered alternatives to Collections Framework classes — used the same way, but safe for concurrent access:

ArrayBlockingQueue ConcurrentHashMap ConcurrentLinkedDeque ConcurrentLinkedQueue ConcurrentSkipListMap ConcurrentSkipListSet CopyOnWriteArrayList CopyOnWriteArraySet DelayQueue LinkedBlockingDeque LinkedBlockingQueue LinkedTransferQueue PriorityBlockingQueue SynchronousQueue

9. Locks

A lock is an object you acquire before touching a shared resource and release afterwards; a second thread that tries to acquire a held lock suspends until it is free. Key methods of the Lock interface:

MethodDescription
void lock()Waits until the lock can be acquired.
void lockInterruptibly()Waits, unless the thread is interrupted.
boolean tryLock()Acquires if free; returns immediately with true/false — never waits.
boolean tryLock(long wait, TimeUnit tu)As above, but waits up to the given time.
Condition newCondition()Returns a Condition (with await()/signal() — like Object.wait()/notify()).
void unlock()Releases the lock.
import java.util.concurrent.locks.*; class LockThread implements Runnable { ReentrantLock lock; String name; LockThread(ReentrantLock lk, String n) { lock = lk; name = n; } public void run() { try { lock.lock(); // acquire Shared.count++; System.out.println(name + ": " + Shared.count); Thread.sleep(1000); } catch (InterruptedException exc) { System.out.println(exc); } finally { lock.unlock(); // ALWAYS release in a finally block } } } ReentrantLock lock = new ReentrantLock(); new Thread(new LockThread(lock, "A")).start(); new Thread(new LockThread(lock, "B")).start();
  • ReentrantLock is the standard implementation — the holding thread may re-enter it, but every lock() needs a matching unlock().
  • ReadWriteLock (impl. ReentrantReadWriteLock) keeps separate read/write locks, allowing multiple concurrent readers when no writer holds it.
  • StampedLock is a specialised lock that implements neither interface but offers Lock-like and ReadWriteLock-like modes.

10. Atomic Operations

java.util.concurrent.atomic lets you get, set, or compare a single variable in one uninterruptible (atomic) step — no lock needed.

import java.util.concurrent.atomic.*; class Shared { static AtomicInteger ai = new AtomicInteger(0); } class AtomThread implements Runnable { String name; AtomThread(String n) { name = n; } public void run() { for(int i = 1; i <= 3; i++) System.out.println(name + " got: " + Shared.ai.getAndSet(i)); // returns old, sets new } }

getAndSet() returns the previous value and sets the new one atomically, so two threads never write ai at the same time. The package also offers lock-free cumulative classes: DoubleAdder, LongAdder (running sum) and DoubleAccumulator, LongAccumulator (user-specified op).

11. The Fork/Join Framework

Traditional multithreading (one CPU) shares a CPU among tasks to use idle time. Parallel programming (multicore) runs pieces of one job truly simultaneously, each on its own CPU. The Fork/Join Framework (JDK 7, in java.util.concurrent): simplifies creating multiple threads and automatically scales to the number of available processors.

11.1 The four core classes

ClassRole
ForkJoinTask<V>Abstract lightweight task (not a thread). Core methods fork() / join().
ForkJoinPoolManages execution of ForkJoinTasks using work-stealing.
RecursiveActionSubclass of ForkJoinTask for tasks with no result: implement protected void compute().
RecursiveTask<V>Subclass for tasks that return a result: implement protected V compute().
final ForkJoinTask<V> fork() // schedule this task asynchronously; caller keeps running final V join() // wait for the task to finish; return its result final V invoke() // fork + join in one call static void invokeAll(ForkJoinTask<?> a, ForkJoinTask<?> b) // run both, wait for both static void invokeAll(ForkJoinTask<?>... taskList)

11.2 The divide-and-conquer strategy

Recursively split a task into subtasks until a subtask is small enough (below a sequential threshold) to process directly. The Java docs suggest a task should perform roughly 100 to 10,000 computational steps; err on the high side for the threshold.

// RecursiveAction: transform an array of doubles into their square roots. class SqrtTransform extends RecursiveAction { final int seqThreshold = 1000; double[] data; int start, end; SqrtTransform(double[] vals, int s, int e) { data = vals; start = s; end = e; } protected void compute() { if((end - start) < seqThreshold) { // small enough: do it directly for(int i = start; i < end; i++) data[i] = Math.sqrt(data[i]); } else { // too big: split in half int middle = (start + end) / 2; invokeAll(new SqrtTransform(data, start, middle), new SqrtTransform(data, middle, end)); // run both subtasks, wait } } } ForkJoinPool fjp = new ForkJoinPool(); SqrtTransform task = new SqrtTransform(nums, 0, nums.length); fjp.invoke(task); // start the main task and wait
Expected output (first 10 elements)
A portion of the original sequence: 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 A portion of the transformed sequence (to four decimal places): 0.0000 1.0000 1.4142 1.7321 2.0000 2.2361 2.4495 2.6458 2.8284 3.0000

11.3 RecursiveTask: returning and aggregating a result

class Sum extends RecursiveTask<Double> { final int seqThresHold = 500; double[] data; int start, end; Sum(double[] vals, int s, int e) { data = vals; start = s; end = e; } protected Double compute() { double sum = 0; if((end - start) < seqThresHold) { for(int i = start; i < end; i++) sum += data[i]; } else { int middle = (start + end) / 2; Sum subA = new Sum(data, start, middle); Sum subB = new Sum(data, middle, end); subA.fork(); // start A asynchronously subB.fork(); // start B asynchronously sum = subA.join() + subB.join(); // wait for both, aggregate } return sum; } } double summation = new ForkJoinPool().invoke(new Sum(nums, 0, nums.length));

Alternatives: subA.fork(); sum = subB.invoke() + subA.join(); or subA.fork(); sum = subB.compute() + subA.join();.

11.4 The common pool

Since JDK 8 you rarely need to build a ForkJoinPool. A static common pool is always available:

ForkJoinPool fjp = ForkJoinPool.commonPool(); // explicit reference // or, simplest of all: task.invoke(); // calling invoke()/fork() outside a pool uses the common pool automatically

11.5 The level of parallelism

ForkJoinPool() // parallelism = number of available processors ForkJoinPool(int pLevel) // pLevel > 0 : max threads that run concurrently (a target, not a guarantee)

On a dual-core machine, running the same transform with parallelism 1 vs 2 (threshold 1000) roughly halved the elapsed time in the book's sample (~260 ms → ~169 ms). Useful queries: getParallelism(), getCommonPoolParallelism(), and Runtime.getRuntime().availableProcessors().

11.6 Work-stealing, daemon threads, and control methods

  • Work-stealing: each worker keeps its own task queue; an idle worker takes tasks from a busy worker's queue — automatic load balancing.
  • ForkJoinPool uses daemon threads, so it need not be explicitly shut down (except a custom pool, via shutdown(); the common pool ignores shutdown()).
  • Async start: execute(ForkJoinTask<?>) or execute(Runnable) (a bridge to traditional threading). Keep the main thread alive until tasks finish.
  • Task status: cancel(boolean), isCancelled(), isCompletedNormally(), isCompletedAbnormally(), isDone(); reinitialize() to run a completed task again (does not undo side effects).
  • Other features: inForkJoinPool(), adapt() (wrap a Runnable/Callable), quietlyJoin()/quietlyInvoke(), tryUnfork(), task tags; pool tuning via toString(), isQuiescent(), getPoolSize(), getActiveThreadCount(), shutdownNow().

11.7 Fork/Join tips

  • Do not set the sequential threshold too low — task creation/switching can cost more than the work. Err high.
  • Usually use the default level of parallelism.
  • A ForkJoinTask should not use synchronized, semaphores, or blocking I/O. (Phaser is compatible.) It should be a pure computation.
  • Make no assumptions about the number of processors or about run-to-run timing.

12. Visual Mental Model

java.util.concurrent | |-- Synchronizers Semaphore | CountDownLatch | CyclicBarrier | Exchanger | Phaser | |-- Executors Executor -> ExecutorService -> ScheduledExecutorService | thread pools Executors.newFixedThreadPool(n) ... -> submit(Callable) -> Future.get() | |-- Concurrent collections ConcurrentHashMap, CopyOnWriteArrayList, *BlockingQueue, ... | |-- locks Lock / ReentrantLock (lock..unlock in finally) | ReadWriteLock | StampedLock |-- atomic AtomicInteger.getAndSet / compareAndSet ; LongAdder ... | `-- Fork/Join ForkJoinPool (work-stealing, common pool, daemon) ForkJoinTask |-- RecursiveAction compute() : void (invokeAll) `-- RecursiveTask<V> compute() : V (fork/fork/join+join) divide-and-conquer down to a sequential threshold

13. Important Comparisons

Traditional threadingConcurrency utilities
LevelLow-level primitivesHigh-level, purpose-built objects
Mutual exclusionsynchronizedLock / ReentrantLock (timed, interruptible, tryLock)
Single-variable updatessynchronized blockAtomic* (lock-free)
Value from a threadShared field + join()Callable + Future
Parallel computationManual splittingFork/Join (auto-scales to cores)
RecursiveActionRecursiveTask<V>
compute() returnvoidV (must return a result)
Typical subtask startinvokeAll(a, b)a.fork(); b.fork(); a.join()+b.join()
CyclicBarrierPhaser
PhasesOne meeting point (reusable)Many phases
PartiesFixed at constructionCan register/deregister dynamically

14. Common Beginner Mistakes

Forgetting es.shutdown() — the JVM never exits.
Calling unlock() outside a finally — an exception leaves the lock held forever.
Mismatched lock()/unlock() counts on a re-entered ReentrantLock.
Using synchronized or blocking I/O inside compute() of a fork/join task.
A sequential threshold that is far too small — overhead swamps the work.
Reusing a consumed synchronizer — a CountDownLatch does not reset (use CyclicBarrier/Phaser).
Expecting exact thread order from the examples — scheduling varies each run.
Assuming a fixed number of CPUs or identical timing across runs.

15. Best Practices

  • Keep using synchronized/wait()/notify() where they already work; add the concurrent API only for extra control or parallelism.
  • Prefer executors + thread pools over creating threads by hand; always shutdown().
  • Use Callable/Future when a task produces a value.
  • Acquire and release locks in try/finally; consider tryLock to avoid indefinite blocking.
  • Use Atomic* when only one variable needs coordination.
  • Pick the right synchronizer for the problem rather than hand-rolling one.
  • For parallel computation, use Fork/Join with a sensible (high-ish) sequential threshold, the default parallelism, and no external blocking/synchronization; the common pool is usually the simplest choice.

16. "Remember" Points

Key takeaways
  • The concurrent API lives in java.util.concurrent (+ .atomic, .locks) and supplements, not replaces, traditional threading.
  • Five synchronizers, each for one job: Semaphore, CountDownLatch, CyclicBarrier, Exchanger, Phaser.
  • Executors + thread pools reuse threads; Callable+Future return values; always shutdown().
  • Lock/ReentrantLock give timed, interruptible, non-blocking mutual exclusion; unlock in finally.
  • Atomic* classes update one variable without a lock.
  • Fork/Join = divide-and-conquer parallelism: extend RecursiveAction (no result) or RecursiveTask<V> (result), split to a sequential threshold, run on a ForkJoinPool (work-stealing, common pool, daemon threads).

17. Interview / Revision Questions

  • What does this chapter mean by a "concurrent program", and why weren't the original primitives enough?
  • Name the three concurrent-API packages and what each provides.
  • How does a Semaphore control access? What do acquire() and release() do? Why does setting the initial permit count matter?
  • Contrast CountDownLatch, CyclicBarrier, and Phaser.
  • What problem does Exchanger solve, and how many threads must call exchange()?
  • What is the purpose of overriding Phaser.onAdvance(), and what does its return value mean?
  • Why use a thread pool? What happens if you forget shutdown()?
  • Compare Callable with Runnable. What does Future.get() do?
  • Why must a lock be released in a finally block? What is a reentrant lock?
  • When would you choose an Atomic* class over a lock?
  • Explain the divide-and-conquer strategy and the role of the sequential threshold.
  • Difference between RecursiveAction and RecursiveTask<V>? How are subtasks started in each?
  • What is work-stealing? Why doesn't a ForkJoinPool usually need shutting down?
  • List three Fork/Join tips from the chapter.

18. Practice Exercises

  • Semaphore: Protect a shared counter with a Semaphore(1) and two threads; then comment out acquire()/release() and observe interleaving.
  • Latch: Start 4 worker threads that each countDown() once; have main await() and print "all workers done".
  • Barrier: Use a CyclicBarrier(3, action) so three threads compute a partial result, meet, and then a barrier action combines them; reuse the barrier for a second round.
  • Executor + Future: Submit three Callables (sum, hypotenuse, factorial) to a fixed pool of 3 and print each result via Future.get().
  • Lock: Rewrite exercise 1 using ReentrantLock with lock()/unlock() in try/finally.
  • Atomic: Have three threads call getAndIncrement() on an AtomicInteger in a loop; confirm the final value is exactly the number of increments.
  • Fork/Join (action): Transform a large double[] into square roots with a RecursiveAction; try thresholds 100, 1000, 10000 and time each with System.nanoTime().
  • Fork/Join (task): Sum a large array with a RecursiveTask<Double> using fork()/join(); then switch it to the common pool via task.invoke().

19. Quick Memory Map

THE CONCURRENCY UTILITIES (java.util.concurrent [+ .atomic, .locks]) | |-- Synchronizers | Semaphore(n[,fair]) acquire()/release() -- permits | CountDownLatch(n) await() / countDown() -- one-shot event count | CyclicBarrier(n[,action]) await() -- reusable meeting point | Exchanger<V> V exchange(V) -- 2-thread swap | Phaser(n) register()/arriveAndAwaitAdvance()/arriveAndDeregister(); | override onAdvance(phase,parties) -> true to terminate | |-- Executors | Executor -> ExecutorService -> ScheduledExecutorService | Executors.newFixedThreadPool(n) / newCachedThreadPool() / newScheduledThreadPool(n) | es.execute(Runnable); es.submit(Callable) -> Future; es.shutdown() // REQUIRED | Callable<V>{ V call() throws Exception } Future<V>.get([wait,TimeUnit]) | TimeUnit: DAYS..NANOSECONDS | |-- Concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList, *BlockingQueue ... | |-- Locks: Lock/ReentrantLock lock()/tryLock([t,u])/unlock() (in finally); newCondition() | ReadWriteLock/ReentrantReadWriteLock ; StampedLock |-- Atomic: AtomicInteger/Long get/set/getAndSet/compareAndSet/incrementAndGet | LongAdder/LongAccumulator (lock-free cumulative) | `-- Fork/Join ForkJoinPool([pLevel]) invoke()/execute() ; ForkJoinPool.commonPool() ForkJoinTask: fork() | join() | invoke() | invokeAll(...) RecursiveAction protected void compute() -> invokeAll(subA, subB) RecursiveTask<V> protected V compute() -> subA.fork(); subB.fork(); return subA.join()+subB.join(); divide-and-conquer to a sequential threshold (~100..10000 steps; err high) work-stealing ; daemon threads ; no synchronized / no blocking I/O inside compute()

20. Complete Chapter Revision

  1. The concurrency utilities (JDK 5+, java.util.concurrent and its .atomic/.locks subpackages) give high-level building blocks for programs that make heavy use of concurrent threads. They supplement the traditional model.
  2. Synchronizers: Semaphore (permit count), CountDownLatch (wait for N events, one-shot), CyclicBarrier (reusable meeting point, optional action), Exchanger (two-thread data swap), Phaser (multi-phase coordination, dynamic parties, overridable onAdvance()).
  3. Executors manage threads: ExecutorExecutorServiceScheduledExecutorService; obtain thread pools from Executors; always call shutdown().
  4. Callable<V> returns a value via Future<V> (get(), optionally timed with TimeUnit).
  5. Concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList, the blocking queues, …) are drop-in safe alternatives to Collections Framework classes.
  6. Locks (Lock/ReentrantLock) give timed, interruptible, non-blocking mutual exclusion and Condition objects; release in finally. ReadWriteLock allows concurrent readers; StampedLock is a specialised variant.
  7. Atomics update a single variable in one uninterruptible step without a lock; adders/accumulators do lock-free cumulative work.
  8. The Fork/Join Framework provides divide-and-conquer parallelism: extend RecursiveAction (no result) or RecursiveTask<V> (result), split to a sequential threshold, and run on a ForkJoinPool (work-stealing, daemon threads, a ready-made common pool). Use the default parallelism, avoid synchronized/blocking I/O in compute(), and don't set the threshold too low.

21. Final Takeaway

The concurrency utilities turn hard, hand-crafted synchronization problems into off-the-shelf objects, and the Fork/Join Framework turns "use all my CPU cores" into extending one class and choosing a split threshold. Keep synchronized for the simple cases, reach for a purpose-built synchronizer, an executor, a lock, or an atomic when you need more control, and use Fork/Join divide-and-conquer when you want genuine parallel speed-up.