A Thorough Understanding of the Design Concept and Underlying Principles of the JUC Toolkit's CountDownLatch

Time: Column:Java views:225

CountDownLatch is a synchronization aid class in Java's concurrency package (java.util.concurrent). It allows one or more threads to wait for a set of operations to complete.

1. Design Concept

CountDownLatch is implemented based on AQS (AbstractQueuedSynchronizer). Its core idea is to maintain a countdown, where the waiting threads will only continue when the countdown reaches zero. The main design goal is to allow multiple threads to coordinate and complete a set of tasks.

  1. Constructor and Counter

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

The count passed during the construction of CountDownLatch determines the initial value of the counter, which controls the release of threads.

  1. Core Operations Supported by AQSAQS is the foundation of CountDownLatch, implemented through a custom internal class Sync. Sync inherits AQS and provides the necessary methods. The key operations are:

  • acquireShared(int arg): If the counter value is zero, it means all tasks are completed, and the thread will be granted permission.

  • releaseShared(int arg): Each time countDown() is called, the counter decreases. When the counter reaches zero, AQS releases all waiting threads.

  1. Implementation Details

  • countDown(): Calls releaseShared() to decrease the counter and notify waiting threads.

  • await(): Calls acquireSharedInterruptibly(1). If the counter is not zero, the thread will block and wait.

2. Underlying Principles

The core of CountDownLatch is based on the AbstractQueuedSynchronizer (AQS), which manages the counter's state. AQS is the foundation for many synchronization tools in JUC, implemented through a synchronizer queue in both exclusive and shared modes for thread management and scheduling. CountDownLatch uses AQS's shared lock mechanism to control multiple threads waiting for a condition.

  1. Shared Mode in AQSAQS provides two synchronization modes: exclusive and shared. CountDownLatch uses the shared mode:

  • Exclusive Mode: Only one thread can hold the lock at a time, such as ReentrantLock.

  • Shared Mode: Allows multiple threads to share the lock, such as Semaphore and CountDownLatch.

The await() and countDown() methods in CountDownLatch correspond to AQS's acquireShared() and releaseShared() operations. acquireShared() checks the synchronization state (counter value), and if it is zero, the thread returns immediately; otherwise, it blocks and enters the waiting queue. releaseShared() is used to decrease the counter and wake up all waiting threads.

  1. Design of the Sync Inner ClassCountDownLatch implements synchronization logic through a private inner class Sync. Sync inherits from AQS and overrides the tryAcquireShared(int arg) and tryReleaseShared(int arg) methods.

static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }

    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    protected boolean tryReleaseShared(int releases) {
        // Spin decrement counter
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}
  • tryAcquireShared(int): Returns 1 (success) when the counter is zero, otherwise returns -1 (block).

  • tryReleaseShared(int): Each call to countDown() decreases the counter. When the counter reaches zero, it returns true, waking up all blocked threads.

  1. CAS Operations Ensure Thread SafetyThe tryReleaseShared method uses CAS (compare-and-set) to update the counter, avoiding the overhead of locks. CAS operations are supported by CPU primitives (e.g., the cmpxchg instruction), achieving efficient, non-blocking operations. This design ensures the thread safety of countDown(), allowing multiple threads to concurrently decrease the counter.

  2. Internal ConditionObjectCountDownLatch is not reusable because AQS's ConditionObject is designed for a one-time trigger. Once the counter reaches zero, CountDownLatch cannot be reset, and all threads are released, without resetting the initial counter value. This is the fundamental reason for its non-reusability.

3. Application Scenarios

  • Waiting for Multiple Threads to Complete Tasks: CountDownLatch is often used in scenarios where multiple threads need to finish their tasks before proceeding, such as batch processing tasks.

  • Parallel Execution and Aggregation: In data analysis or computation-intensive tasks, tasks are split into multiple sub-tasks for parallel execution, with the main thread waiting for all sub-tasks to complete before aggregating the results.

  • Multi-Service Dependency Coordination: When one service depends on multiple others, CountDownLatch can be used to synchronize calls across services, ensuring that all dependent services are ready before executing the main task.

4. Example Code

The following example demonstrates how to use CountDownLatch to implement a mechanism where the main thread waits for all child tasks to complete.

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    private static final int TASK_COUNT = 5;
    private static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_COUNT; i++) {
            new Thread(new Task(i + 1, latch)).start();
        }

        // Main thread waits for all tasks to complete
        latch.await();
        System.out.println("All tasks completed, proceeding with main thread tasks");
    }

    static class Task implements Runnable {
        private final int taskNumber;
        private final CountDownLatch latch;

        Task(int taskNumber, CountDownLatch latch) {
            this.taskNumber = taskNumber;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                System.out.println("Sub-task " + taskNumber + " started");
                Thread.sleep((int) (Math.random() * 1000)); // Simulate task execution time
                System.out.println("Sub-task " + taskNumber + " completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                latch.countDown(); // Decrease counter when a task completes
            }
        }
    }
}

5. Comparison with Other Synchronization Tools

  1. CyclicBarrier

    • Principle and Use: CyclicBarrier also allows a group of threads to wait for each other until all threads reach a barrier point. It is suitable for multi-stage tasks or aggregating results after each stage.

    • Underlying Implementation: CyclicBarrier is implemented using ReentrantLock and Condition, and the barrier count can be reset, allowing it to be reused.

    • Comparison with CountDownLatch: CyclicBarrier is reusable, making it suitable for repeated synchronization, while CountDownLatch is one-time use. CountDownLatch is more flexible, allowing any thread to call countDown(), whereas CyclicBarrier requires specified threads to reach the barrier.

  2. Semaphore

    • Principle and Use: Semaphore is mainly used to control the concurrent number of resources accessed, such as limiting access to a database connection pool.

    • Underlying Implementation: Semaphore is based on AQS’s shared mode, similar to CountDownLatch but allows resource control through a set number of “permits”.

    • Comparison with CountDownLatch: Semaphore can dynamically increase/decrease permits, while CountDownLatch only decrements. Semaphore is suitable for controlling access limits, while CountDownLatch is used for synchronizing count-down points.

  3. Phaser

    • Principle and Use: Phaser is an enhanced version of CyclicBarrier, allowing dynamic adjustment of the number of participating threads. It is suitable for multi-stage task synchronization and can add/remove participating threads at any time.

    • Underlying Implementation: Phaser includes a counter for managing the number of participating threads in each phase, allowing tasks to dynamically register or unregister.

    • Comparison with CountDownLatch: Phaser is more suitable for complex scenarios that require flexible control of phases and threads, while CountDownLatch is simpler and used for one-time synchronization.

6. Summary

CountDownLatch is a lightweight, non-reusable countdown synchronizer suitable for simple, one-time thread coordination. Its AQS-based shared lock implementation ensures efficient concurrent thread management and counter updates. While it lacks reusability, its simplicity makes it ideal for scenarios requiring the completion of multiple tasks.

Compared to other JUC tools:

  • CyclicBarrier is better for multi-stage synchronization and stage-based aggregation.

  • Semaphore is suited for resource access control with a controllable number of permits.

  • Phaser is more flexible and suitable for dynamic participation and complex multi-stage tasks.

Choosing the right synchronization tool depends on the task's nature, the dynamic involvement of threads, and whether synchronization control needs to be reused.