概述
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此,在构造CountDownLatch的时候需要传入一个整数n,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
CountDownLatch使用
CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁
public static void main(String[] args) {
//设置CountDownLatch计数器为1
final CountDownLatch latch = new CountDownLatch(1);
Thread thread1 = new Thread() {
@Override
public void run() {
try {
latch.await();
System.out.println("do other thing");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
Thread thread2 = new Thread() {
public void run() {
try {
Thread.sleep(1000);
//计数器-1
latch.countDown();
System.out.println("释放Latch");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
thread1.start();
thread2.start();
}
打印结果:
释放Latch
do other thing
CountDownLatch源码剖析
CountDownLatch的数据结构
从源码可知,其底层是由AQS提供支持,所以其数据结构可以参考AQS的数据结构,而AQS的数据结构核心就是两个虚拟队列:同步队列sync queue 和条件队列condition queue,不同的条件会有不同的条件队列。它可以用来实现可以依赖 int 状态的同步器,获取和释放参数以及一个内部FIFO等待队列深入理解AQS原理
CountDownLatch源码
CountDownLatch(int count)
构造一个用给定计数初始化的 CountDownLatch。
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
void await()
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
boolean await(long timeout, TimeUnit unit)
// 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
void countDown()
// 返回当前计数。
long getCount()
// 返回标识此锁存器及其状态的字符串。
String toString()
构造函数:
//count小于0,则抛出异常
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
内部类Sync,Sync继承于AQS类
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//Sync构造函数
Sync(int count) {
setState(count);
}
//在AQS中,state是一个private volatile long类型的对象。
//对于CountDownLatch而言,state表示的”锁计数器“。
//CountDownLatch中的getCount()
//最终是调用AQS中的getState(),返回的state对象,即”锁计数器“。
int getCount() {
return getState();
}
//尝试获取共享锁
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -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;
}
}
}
//CountDownLatch持有AQS锁的成员变量
private final Sync sync;
Await()方法
调用 Await() 方法时,先去获取 state 的值,当计数器不为0的时候,说明还有需要等待的线程在运行,则调用 doAcquireSharedInterruptibly 方法,进来执行的第一个动作就是尝试加入等待队列 ,即调用 addWaiter()方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//AQS中方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取共享锁,该方法定义在Sync中
if (tryAcquireShared(arg) < 0)
//若锁获取失败则调用此方法
doAcquireSharedInterruptibly(arg);
}
//Sync中尝试获取共享锁
protected int tryAcquireShared(int acquires) {
//计数器的值为0则获取成功,否则失败返回-1
return (getState() == 0) ? 1 : -1;
}
假设此时共享锁获取失败,则开始调用doAcquireSharedInterruptibly()方法
将当前线程加入等待队列,并通过 parkAndCheckInterrupt()方法实现当前线程的阻塞
//AQS中模板方法
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将当前节点加入等待队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取队列中当前节点的前一个节点,如果为head,则继续尝试获取锁
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//获取成功,设置当前节点为head,并唤醒该线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//判断当前节点是否需要阻塞,如果当前节点的前继节点不是head,则需要
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞线程
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//唤醒节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//将此节点设置为头节点
setHead(node);
//propagate也就是state的更新值大于0,代表可以继续acquire
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//判断后继节点是否存在,如果存在是否是共享模式的节点
//进行共享模式的释放
//在这里h头节点进行了两次判定,第一次是判定旧头节点存在且状态已经被设置过,
//第二次是判定设置后的头节点是否存在并且状态已经被设置过。
//只有满足上述的一个条件,就会对其后继节点做判断。
//后继节点不存在或者后继节点是共享模式,那就可以对整个队列进行释放操作。
if (s == null || s.isShared())
doReleaseShared();
}
}
//释放线程,实际就是设置waitStatus为0
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//共享模式和独占模式都是满足SIGNAL信号才能唤醒后继节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//CAS操作成功则解除阻塞
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//在循环过程中,为了防止在上述操作中添加了新节点的情况
//通过检测头节点是否改变,如果改变了就继续循环
if (h == head) // loop if head changed
break;
}
}
//解除阻塞
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
//如果s为null且当前状态为CANCELLED,则从尾部开始迭代寻找一个不为null且waitStatus设置过,即小于0的数作为要操作的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//调用Unsafe类的unpark,解除阻塞
//这是CPU级别的操作
if (s != null)
LockSupport.unpark(s.thread);
}
调用await()方法的线程想要获取锁,有两个条件:
- 所有持有该锁的线程释放此锁-计数器为0
- 当前线程被唤醒后,CLH队列中当前线程的前一个线程处于队列的head处。或者队列为空。*
countdonw()释放锁
当计数器为 0 后,会唤醒等待队列里的所有线程,所有调用了 await() 方法的线程都被唤醒,并发执行
public void countDown() {
sync.releaseShared(1);
}
//Sync中方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//释放CLH队列中head节点的后驱节点
doReleaseShared();
return true;
}
return false;
}
//Sync中方法
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
//不断自旋获取锁的状态,如果计数器为0,则跳出自旋
//计数器为0说明锁根本不被占用
int c = getState();
if (c == 0)
return false;
//计数-1
int nextc = c-1;
//更新锁的状态
if (compareAndSetState(c, nextc))
//当计数器值为0,返回true,可以释放await的线程
return nextc == 0;
}
}
使用countdown,每次使用计数器值-1,当计数器值为0,释放线程。
总结
- AQS分为共享模式和独占模式,CountDownLacth使用了它的共享模式
- AQS 当第一个等待线程(被包装为 Node)要入队的时候,要保证存在一个 head 节点,这个 head 节点不关联线程,也就是一个虚节点。
- 当队列中的等待节点(关联线程的,非 head 节点)抢到锁,将这个节点设置为 head 节点
- 第一次自旋抢锁失败后,waitStatus 会被设置为 -1(SIGNAL),第二次再失败,就会被 LockSupport 阻塞挂起
- 如果一个节点的前置节点为 SIGNAL 状态,则这个节点可以尝试抢占锁
- 当要释放某个节点时,先将当前节点的waitStatus置为0