Concurrent Utility Classes in Java: Principles and Practical Applications

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):

  • CountDownLatch maintains 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

  1. Main thread waits for multiple child threads to finish initialization/task execution;
  2. 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

  1. Multi-threaded phase tasks: For example, "data loading → data calculation → data aggregation", where all threads complete each phase before proceeding;
  2. 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

  1. Rate limiting: For example, limiting the number of threads accessing a database connection pool;
  2. Resource pool management: Controlling the number of threads or connections used simultaneously;
  3. 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

  1. Data verification: Two threads compute the same batch of data results, exchange, and verify;
  2. 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

  1. CountDownLatch: One-time wait, suitable for scenarios like "main thread waiting for multiple threads to finish";
  2. CyclicBarrier: Reusabel mutual wait, suitable for scenarios like "multi-threaded phase collaboration";
  3. Semaphore: License-based rate limiting, suitable for scenarios like "controlling the number of threads accessing resources";
  4. 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.

Tags: concurrent programming java utilities Thread Synchronization Java Concurrency

Posted on Fri, 05 Jun 2026 17:28:35 +0000 by ploiesti