Java's concurrent utility classes (JUC) provide ready-to-use concurrency control capabilities, avoiding the need to reinvent the wheel. The CountDownLatch, CyclicBarrier, Semaphore, and Exchanger are the most essential four. This article will explain each tool from core purpose, underlying mechanism, use cases, and code examples to help you understand and apply them effectively.
CountDownLatch: Countdown Latch (One-time Use)
Core Purpose
Allows one or more threads to wait for other threads to complete a set of operations before proceeding (e.g., main thread waiting for all child threads to finish initialization).
- Key characteristic: One-time use, the counter cannot be reset after reaching zero; a new object must be created.
Underlying Mechanism
Based on AQS (AbstractQueuedSynchronizer):
CountDownLatchmaintains an internal counter initialized with a specified value;- Calling
countDown()decrements the counter (AQS releases a shared lock); - Calling
await()blocks the thread until the counter reaches zero (AQS acquires a shared lock successfully).
Typical Use Cases
- Main thread waits for multiple child threads to finish initialization/task execution;
- Concurrent testing: Wait for all test threads to start before beginning timing.
Code Example
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
// Initialize counter to 3
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 1; i <= threadCount; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("Task " + taskId + " started");
Thread.sleep(1000); // Simulate task execution
System.out.println("Task " + taskId + " completed");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Decrement counter
latch.countDown();
}
}).start();
}
// Main thread waits for all child threads to finish
latch.await();
System.out.println("All tasks completed, main thread continues");
}
}
Execution Output:
Task 1 started
Task 2 started
Task 3 started
Task 1 completed
Task 2 completed
Task 3 completed
All tasks completed, main thread continues
CyclicBarrier: Cyclic Barrier (Reusable)
Core Purpose
Allows a group of threads to wait for each other until all reach a certain point, then proceed together (e.g., multi-threaded computation followed by unified result aggregation).
- Key characteristic: Reusable, the counter can be reset and reused; supports setting a 'barrier action' that executes after all threads arrive.
Underlying Mechanism
Based on ReentrantLock + Condition (not directly using AQS):
- Maintains the number of waiting threads and the barrier count (number of threads reaching the barrier);
- When a thread calls
await(), it enters a waiting state until the number of waiting threads equals the barrier count; - After all threads arrive, the barrier action is executed (if any), and the counter is reset to begin the next round.
Typical Use Cases
- Multi-threaded phase tasks: For example, "data loading → data calculation → data aggregation", where all threads complete each phase before proceeding;
- Simulation of concurrency: Let multiple threads start executing simultaneously.
Code Example
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
// Initialize barrier: execute action when 3 threads arrive
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads reached the barrier point, executing aggregation");
});
for (int i = 1; i <= threadCount; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("Thread " + taskId + " performing phase 1 task");
Thread.sleep(1000);
System.out.println("Thread " + taskId + " reached the barrier");
// Wait for other threads to arrive
barrier.await();
// All threads arrived, perform phase 2 task
System.out.println("Thread " + taskId + " performing phase 2 task");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
Execution Output:
Thread 1 performing phase 1 task
Thread 2 performing phase 1 task
Thread 3 performing phase 1 task
Thread 1 reached the barrier
Thread 2 reached the barrier
Thread 3 reached the barrier
All threads reached the barrier point, executing aggregation
Thread 3 performing phase 2 task
Thread 1 performing phase 2 task
Thread 2 performing phase 2 task
Semaphore: Semaphore (Rate Limiting/Resource Control)
Core Purpose
Controls the number of threads accessing a specific resource (rate limiting) using a license mechanism:
- Key characteristic: Supports 'fair/non-fair' license acquisition; allows dynamic adjustment of license count (
release(n)).
Underlying Mechanism
Based on AQS:
- Initializes with a license count (AQS's state variable);
- Calling
acquire()attempts to get one license (AQS acquires a shared lock), blocking if no licneses are available; - Calling
release()frees one license (AQS releases a shared lock), waking up waiting threads.
Typical Use Cases
- Rate limiting: For example, limiting the number of threads accessing a database connection pool;
- Resource pool management: Controlling the number of threads or connections used simultaneously;
- Simulating concurrency: Controlling the number of threads executing at the same time.
Code Example
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permits = 2; // Maximum of 2 threads executing at once
// Non-fair lock (default), use new Semaphore(2, true) for fair lock
Semaphore semaphore = new Semaphore(permits);
for (int i = 1; i <= 5; i++) {
int taskId = i;
new Thread(() -> {
try {
// Acquire license
semaphore.acquire();
System.out.println("Thread " + taskId + " acquired license, starting execution");
Thread.sleep(1000); // Simulate task execution
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Release license
semaphore.release();
System.out.println("Thread " + taskId + " released license");
}
}).start();
}
}
}
Execution Output (Key: Only two threads executing at the same time):
Thread 1 acquired license, starting execution
Thread 2 acquired license, starting execution
Thread 1 released license
Thread 3 acquired license, starting execution
Thread 2 released license
Thread 4 acquired license, starting execution
Thread 3 released license
Thread 5 acquired license, starting execution
Thread 4 released license
Thread 5 released license
Exchanger: Thread Data Exchange
Core Purpose
Allows two threads to exchange data at a specific point (one-to-one exchange). If only one thread arrives, it will block until another thread arrives.
- Key characteristic: Only supports two threads; can set a timeout (
exchange(V x, long timeout, TimeUnit unit)).
Underlying Mechanism
Based on CAS + Node (stores waiting threads and exchanged data):
- When thread A calls
exchange(), it creates a Node and stores data, then spins waiting; - When thread B calls
exchange(), it finds thread A's Node, exchanges data, wakes thread A, and both return simultaneously.
Typical Use Cases
- Data verification: Two threads compute the same batch of data results, exchange, and verify;
- Producer-consumer: Simple one-to-one data exchange (replacing queues).
Code Example
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
// Thread 1: Produces data
new Thread(() -> {
try {
String data1 = "Thread 1's result";
System.out.println("Thread 1 preparing to exchange data: " + data1);
// Wait for thread 2 to exchange data
String data2 = exchanger.exchange(data1);
System.out.println("Thread 1 received exchanged data: " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Thread 2: Produces data
new Thread(() -> {
try {
String data2 = "Thread 2's result";
System.out.println("Thread 2 preparing to exchange data: " + data2);
// Wait for thread 1 to exchange data
String data1 = exchanger.exchange(data2);
System.out.println("Thread 2 received exchanged data: " + data1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Execution Output:
Thread 1 preparing to exchange data: Thread 1's result
Thread 2 preparing to exchange data: Thread 2's result
Thread 1 received exchanged data: Thread 2's result
Thread 2 received exchanged data: Thread 1's result
Summary of Key Differences Among the Four Tools
| Tool Class | Primary Goal | Characteristics | Underlying Dependency |
|---|---|---|---|
| CountDownLatch | One or more threads wait for other threads to finish | One-time use, non-resettable | AQS (Shared Lock) |
| CyclicBarrier | Multiple threads wait for each other at a barrier point | Reusable, supports barrier actions | ReentrantLock + Condition |
| Semaphore | Control the number of threads accessing resources | Fair/non-fair, adjustable | AQS (Shared Lock) |
| Exchanger | Two threads exchange data | One-to-one, supports timeout | CAS + Node |
Conclusion
- CountDownLatch: One-time wait, suitable for scenarios like "main thread waiting for multiple threads to finish";
- CyclicBarrier: Reusabel mutual wait, suitable for scenarios like "multi-threaded phase collaboration";
- Semaphore: License-based rate limiting, suitable for scenarios like "controlling the number of threads accessing resources";
- Exchanger: One-to-one data exchange, suitable for scenarios like "two threads exchanging data for verification or simple communication".
These tools are core components of JUC packages. Most are based on AQS or locking mechanisms. Mastering them avoids writing complex synchronization logic, improving the readability and stability of concurrent code.