J.U.C锁之 CountDownLatch
1 CountDownLatch 简介
CountDownLatch 名为"闭锁"
主要特性
CountDownLatch它允许一个或多个线程一直等待,直到其他线程执行完后再执行。这里存在两个角色 "等待者" 和 "通知"。等待者调用
CountDownLatch.awit函数后会阻塞当前线程,等待通知去释放。
应用场景
-
实现最大的并行性当我们想同时启动多个线程,实现最大程度的并行性一起等待通知。
-
开始执行前等待 N 个线程完成各自任务
-
死锁检测,用 N 个线程去访问共享资源,在每个测试阶段线程数量不同,并尝试产生死锁。
常用API
/**
* 阻塞等待唤醒
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* 阻塞等待唤醒,添加超时机制
*/
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/**
* 释放一次通知
*/
public void countDown() {
sync.releaseShared(1);
}
2 实现原理
CountDownLatch 使用AQS实现锁机制,AQS是AbstractQueuedSynchronizer的缩写,翻译过来就是"同步器",,它实现了Java函数中锁同步(synchronized),锁等待(wait,notify)功能。
AbstractQueuedSynchronizer是一个抽象类,我们可以编写自己类继承AQS重写获取独占式或共享式同步状态模板方法,实现锁锁同步(synchronized),锁等待(wait,notify)功能
2.1 AQS 实现原理
AQS核心是一个同步状态,两个队列。它们实现了Java函数中锁同步(synchronized),锁等待(wait,notify),并在其基础上实现了独占式同步,共享式同步2中方式锁的实现。
无论独占式还时共享式获取同步状态成功则直接返回,失败则进入CLH同步队列并阻塞当前线程。当获取同步状态线程释放同步状态,AQS会选择从CLH队列head头部节点的第一个节点释放阻塞,尝试重写竞争获取同步状态,如果成功则将当前节点出队。如果失败则继续阻塞。
获取同步状态的线程也可以使用condition对象释放同步状态进入等待队列。只有等待其他线程使用condition.signal或condition.signAll()唤醒被从阻塞状态中释放重新竞争获取同步状态成功后从原来指令位置继续运行。
2.1.1 同步状态
AQS实现了锁,必然存在一个竞争资源。AQS存在从一个int类型的成员变量state,我们把它称为同步状态,同步状态通常用做判断线程能否获取锁的依据
2.1.2 同步队列
AQS 实现了锁那么总需要一个队列将无法获取锁的线程保存起来,方便在锁释放时通知队列中线程去重新竞争锁。
实现原理
同步队列又被称为CLH同步队列,CLH队列是通过链式方式实现FIFO双向队列。当线程获取同步状态失败时,AQS则会将当前线程构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态被释放时,会把首节点后第一个节点的线程从阻塞状态下唤醒,唤醒的线程会尝试竞争同步状态,如果能获取同步状态成功,则从同步队列中出队。
2.1.3 Condition & 等待队列
-
Java 传统的监视器有如下函数 wait、notify、notifyAll。它们可以实现当一个线程获取锁时,它可以主动放弃锁进入一个条件队列中。只有其他线程通知时才从条件队列中出队,重新获取锁成功后继续执行之前的未完成代码逻辑。
-
AQS内部存在一个内部类实现了Condition接口,其内部维护着一条链式实现单向等待队列。我们可以使用AQS获取内部实现Condition接口对象,调用await(),signal(),signalAll()函数实现Java中wait、notify、notifyAll同样功能。
实现原理
- 当获取同步状态的线程调用condition.await(),则会阻塞,并进入一个等待队列,释放同步状态.
- 当其他线程调用了condition.signal()方法,会从等待队列firstWaiter开始选择第一个等待状态不是取消的节点.添加到同步队列尾部.
- 当其他线程调用了condition.signalAll()方法,会从等待队列firstWaiter开始选择所有等待状态不是取消的节点.添加到同步队列尾部.
这里取消节点表示当前节点的线程不在参与排队获取锁。
image2.1.4 独占式同步
从概念上来说独占式对应只存在一个资源,且只能被一个线程或者说竞争者占用.
2.1.5 共享式同步
从概念上来说共享式对应存在多个资源的是有多个线程或者竞争者能够获取占用.
2.2 模板方法
我们可以编写自己类继承AQS选择重写独占式或共享式模板方法,从而定义如何获取同步状态和释放同步状态的逻辑。
2.2.1 独占式
tryAcquire:尝试独占式获取同步状态,返回值为true则表示获取成功,否则获取失败。
tryRelease:
尝试独占式释放同步状态,返回值为true则表示获取成功,否则获取失败。
2.2.2 共享式
tryAcquireShared:尝试共享式获取同步状态,当返回值为大于等于0的时获得同步状态成功,否则获取失败。
tryReleaseShared:尝试共享式释放同步状态,返回值为true则表示获取成功,否则获取失败。
CountDownLatch 内部存在有一个内部类Sync继承AbstractQueuedSynchronizer
ReentrantLock很多方法都通过代理内部类的方法实现。
AQS实现原理
-
AQS核心是一个同步状态,两个队列。它们实现了Java函数中锁同步(synchronized),锁等待(wait,notify),并在其基础上实现了独占式同步,共享式同步2中方式锁的实现。
-
无论独占式还时共享式获取同步状态成功则直接返回,失败则进入CLH同步队列并阻塞当前线程。当获取同步状态线程释放同步状态,AQS会选择从CLH队列head头部节点的第一个节点释放阻塞,尝试重写竞争获取同步状态,如果成功则将当前节点出队。如果失败则继续阻塞。
-
我们可以编写自己类继承AQS选择重写独占式或共享式模板方法,从而定义如何获取同步状态和释放同步状态的逻辑。
如何基于AQS实现
由于是可以多个线程同时等待,当然我们选择使用共享同步,Sync需要重写 tryAcquire 获取同步状态条件逻辑,tryRelease释放同步条件逻辑。其核心点在于初始化的时候我们会设置一个同步状态初始化值,每当调用await则获取共享锁,如果同步状态不为0则阻塞。每当调用countDown则释放共享锁,每次释放共享锁就对同步状态-1,当同步状态为0时释放锁成功。等待者从阻塞中被唤醒。
类结构
CountDownLatch 使用AQS实现锁机制,CountDownLatch 内部存在有一个内部类Sync继承AbstractQueuedSynchronizer
CountDownLatch所有方法都通过代理内部类的方法实现。
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
/** 实例化Sync,设置同步状态 **/
Sync(int count) {
setState(count);
}
/** 获取同步状态 **/
int getCount() {
return getState();
}
/**
* 获取同步状态
* 同步状态为0时释放获取同步状态成功,否则失败
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
/**
* 释放同步状态
*
* 使用CAS+循环将同步状态-1
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;