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
| Package | Contents |
| java.util.concurrent | Core features: synchronizers, executors, concurrent collections, the Fork/Join Framework, the TimeUnit enumeration, and (JDK 9+) the Flow reactive-streams subsystem. |
| java.util.concurrent.atomic | Lock-free updates of single variables via classes like AtomicInteger, AtomicLong and methods like compareAndSet(), getAndSet(), decrementAndGet(). |
| java.util.concurrent.locks | An 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.
| Class | Purpose |
| Semaphore | Classic counting semaphore: controls access to a resource through a count of permits. |
| CountDownLatch | Waits until a specified number of events have occurred. |
| CyclicBarrier | Lets a group of threads wait at a predefined point until all have reached it; reusable. |
| Exchanger | Exchanges data between exactly two threads. |
| Phaser | Synchronizes 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.
| Type | Role |
| Executor | Base interface: void execute(Runnable thread) starts a thread. |
| ExecutorService | Extends Executor; adds shutdown(), methods that run value-returning tasks, run sets of tasks, and report status. |
| ScheduledExecutorService | Extends ExecutorService to support scheduling. |
| Implementations | ThreadPoolExecutor, 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:
| Method | Description |
| 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
| Class | Role |
| ForkJoinTask<V> | Abstract lightweight task (not a thread). Core methods fork() / join(). |
| ForkJoinPool | Manages execution of ForkJoinTasks using work-stealing. |
| RecursiveAction | Subclass 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 threading | Concurrency utilities |
| Level | Low-level primitives | High-level, purpose-built objects |
| Mutual exclusion | synchronized | Lock / ReentrantLock (timed, interruptible, tryLock) |
| Single-variable updates | synchronized block | Atomic* (lock-free) |
| Value from a thread | Shared field + join() | Callable + Future |
| Parallel computation | Manual splitting | Fork/Join (auto-scales to cores) |
| RecursiveAction | RecursiveTask<V> |
| compute() return | void | V (must return a result) |
| Typical subtask start | invokeAll(a, b) | a.fork(); b.fork(); a.join()+b.join() |
| CyclicBarrier | Phaser |
| Phases | One meeting point (reusable) | Many phases |
| Parties | Fixed at construction | Can 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
- 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.
- 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()).
- Executors manage threads: Executor → ExecutorService → ScheduledExecutorService; obtain thread pools from Executors; always call shutdown().
- Callable<V> returns a value via Future<V> (get(), optionally timed with TimeUnit).
- Concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList, the blocking queues, …) are drop-in safe alternatives to Collections Framework classes.
- 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.
- Atomics update a single variable in one uninterruptible step without a lock; adders/accumulators do lock-free cumulative work.
- 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.