【深入JUC原理】:CountDownLatch源码剖析与工作原理完全解析
发布时间: 2024-10-21 23:52:15 阅读量: 31 订阅数: 29
Java并发包源码分析(JDK1.8)
![CountDownLatch](https://datmt.com/wp-content/uploads/2022/09/Java-Concurrency-CountDownLatch-Tutorial-1024x536.jpg)
# 1. CountDownLatch的简介与作用
## 1.1 CountDownLatch的定义
CountDownLatch是Java并发包(java.util.concurrent)中的一个实用工具类,它允许一个或多个线程等待其他线程完成操作。通过它可以实现让一个线程阻塞,直到其他线程完成一系列操作后,这个线程才继续执行。这种同步辅助类在需要进行等待直到某个条件成立时非常有用,尤其适用于启动和关闭多线程应用程序中的主方法。
## 1.2 CountDownLatch的作用场景
CountDownLatch在多线程环境下可用于:
- 开始执行前等待其他线程初始化操作完成。
- 等待某个事件发生,例如多个线程完成数据处理任务后,主线程再进行汇总。
- 测试时模拟多线程阻塞。
通过提供一个初始的计数值,调用`countDown()`方法会减少计数,而调用`await()`方法的线程会阻塞直到计数到达零。CountDownLatch是不可重置的,一旦计数到零,后续调用`await()`不会阻塞。
# 2. CountDownLatch核心概念与架构
## 2.1 CountDownLatch工作原理概述
### 2.1.1 CountDownLatch的功能描述
CountDownLatch是Java.util.concurrent包下的一个同步工具类,它允许一个或多个线程等待直到在其他线程中执行的一组操作完成。其设计理念在于,主执行线程在启动其他线程工作前等待,直到所有工作线程通知它们已经准备就绪,这时主执行线程才继续执行。
CountDownLatch通过一个初始计数值进行操作。这个计数值可以被设置为任何正整数,并且在构造时初始化。线程调用`countDown()`方法来减少这个计数值,而调用`await()`方法的线程将会阻塞直到计数值到达零。计数达到零后,`await()`方法的阻塞会立即释放,且后续调用`await()`将不会阻塞。
### 2.1.2 CountDownLatch与其它同步器的比较
CountDownLatch和其他并发同步器相比,特别是在CyclicBarrier和Semaphore中,表现出其独特性。和CyclicBarrier不同的是,CountDownLatch不可重置,也就是说一旦计数到达零,它无法重新被初始化用于之后的同步。而CyclicBarrier可以多次使用,适合于需要重复执行的同步屏障场景。
相比于Semaphore,CountDownLatch并不是用来限制对共享资源的访问,而是用来协调线程间的同步。Semaphore更适用于控制资源的访问数量,而CountDownLatch更多地用于执行前的等待和执行后的继续。
## 2.2 CountDownLatch内部结构分析
### 2.2.1 AQS(AbstractQueuedSynchronizer)简介
CountDownLatch内部使用了Java并发包中的AQS(AbstractQueuedSynchronizer)来实现其功能。AQS是一个用于构建锁和同步器的框架,它使用一个int成员变量表示同步状态,并通过内置的FIFO队列来管理线程的排队工作。
AQS定义了一套多线程访问共享资源的框架,包括独占式和共享式两种获取资源的方式,而CountDownLatch正是利用了AQS的共享模式。在共享模式下,多个线程可以同时访问资源,而CountDownLatch正是允许多个线程同时进行`countDown()`操作。
### 2.2.2 CountDownLatch的同步状态管理
CountDownLatch初始化时,会将AQS的同步状态设置为传入构造函数的计数值。每次调用`countDown()`,同步状态就会减少1,直到状态为0。在内部,同步状态的修改是通过CAS(Compare-And-Swap)操作来保证原子性。
### 2.2.3 CountDownLatch中的等待队列管理
当线程调用`await()`方法时,AQS会将其封装为一个Node节点,并添加到等待队列中。这个节点会根据线程是否应该被阻塞而保持在队列中,或者被移除。当计数器减到0时,等待队列中的线程会被唤醒。
## 2.3 CountDownLatch使用场景与案例分析
### 2.3.1 CountDownLatch的典型使用案例
一个典型的场景是在主程序启动后,需要等待多个服务组件都启动完成后才能继续执行。例如,在大型系统中,可能需要启动多个后台服务,只有所有服务都启动就绪后,主线程才能进入主循环进行工作。
```java
public class Main {
public static void main(String[] args) {
int workerCount = 5;
CountDownLatch doneSignal = new CountDownLatch(workerCount);
for (int i = 0; i < workerCount; i++) {
new Thread(new WorkerTask(doneSignal)).start();
}
try {
doneSignal.await(); // 主线程等待所有工作线程完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有工作线程已经就绪,主线程继续执行...");
}
}
class WorkerTask implements Runnable {
private final CountDownLatch doneSignal;
WorkerTask(CountDownLatch doneSignal) {
this.doneSignal = doneSignal;
}
public void run() {
doWork();
doneSignal.countDown(); // 通知计数减一
}
private void doWork() {
System.out.println(Thread.currentThread().getName() + " 正在执行工作");
// 模拟任务执行
try {
Thread.sleep((long)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
### 2.3.2 性能影响因素和实际应用场景讨论
CountDownLatch的性能受到多个因素的影响,比如等待线程的数量、`await()`和`countDown()`方法的调用频率、以及线程调度的开销等。在应用中,CountDownLatch的计数初始值通常设置为工作线程的数量,这样可以确保所有线程都完成任务后主线程才继续执行。
实际场景中,应避免在高频调用的循环内部调用`await()`和`countDown()`,因为这些操作可能引发上下文切换和线程调度的开销。在设计应用时,可以通过预先启动线程并在启动后立即进行`countDown()`调用来减少这些开销,以提升性能。
# 3. CountDownLatch源码深度剖析
## 3.1 CountDownLatch源码结构总览
### 3.1.1 构造函数解析
CountDownLatch的构造函数非常简洁,只接收一个int类型的参数`count`,这个参数表示在调用`countDown()`方法时需要减到0之前,等待线程需要进行多少次减法操作。在内部,这个`count`值被存储在一个同步状态中,CountDownLatch使用了AQS的状态来实现计数功能。
```java
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
```
在这段代码中,如果传入的`count`小于0,则会抛出一个`IllegalArgumentException`异常。`sync`对象是CountDownLatch的内部类`Sync`的实例,它继承自`AbstractQueuedSynchronizer`。
### 3.1.2 主要方法实现细节
CountDownLatch提供了两个核心方法:`await()`和`countDown()`。`await()`方法使得线程在计数到达0之前一直等待,除非线程被中断或超时。`countDown()`方法将计数器减1,当计数器为0时,释放所有等待的线程。
```java
public void await() throws InterruptedException {
sync.acquireShared(1);
}
```
`await()`方法调用了AQS的`acquireShared(1)`方法,它会使得当前线程进入等待状态直到计数器为0。而`countDown()`方法则调用了`releaseShared(1)`,这会释放等待的线程。
```java
public void countDown() {
sync.releaseShared(1);
}
```
这两个方法的实现是基于AQS共享模式的API,确保了线程安全的加减操作。
## 3.2 CountDownLatch的等待机制源码解析
### 3.2.1 await()方法的工作原理
`await()`方法是使得当前线程在计数到达0之前一直处于等待状态,其核心源码如下:
```java
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
```
这里使用的是`acquireSharedInterruptibly`方法,它是可中断的获取共享资源的方式。在中断或超时时,线程会从等待中醒来,以响应中断请求。
```java
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
```
这段代码首先检查当前线程是否被中断,如果是,则抛出`InterruptedException`异常。然后尝试获取共享锁,如果获取失败(即计数不为0),则将线程加入到等待队列中。
### 3.2.2 countDown()方法的作用及其实现
`countDown()`方法用于减小计数,当计数减至0时释放所有等待线程。其源码实现是:
```java
public void countDown() {
sync.releaseShared(1);
}
```
这个方法调用了AQS的`releaseShared(1)`方法,该方法会尝试释放共享资源。
```java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
```
`tryReleaseShared(arg)`尝试释放资源并返回是否应该唤醒其他等待线程,如果返回`true`,则调用`doReleaseShared()`方法唤醒后继节点。
### 3.2.3 计数到达0后的逻辑处理
当计数器的值减到0时,所有等待的线程都会被唤醒。这是通过AQS的共享模式唤醒机制完成的。具体来说,在`doReleaseShared()`方法中,会唤醒头节点的后继节点,而头节点是获取到资源的节点。
```java
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // loop if head changed
break;
}
}
```
这段代码确保了线程安全的唤醒机制,它会不断地尝试更新等待状态并唤醒后继节点直到状态设置成功。
## 3.3 CountDownLatch的异常处理与安全性分析
### 3.3.1 异常情况下Cou
0
0