目录
前言
正文
CountDownLatch使用场景
CountDownLatch简单的使用
CountDownLatch源码解读
CountDownLatch结构和构造方法
await()方法
countDown()方法
总结
目前也是金三银四跳槽找工作的最好时机,可能很多小伙伴在面试中被面试官问到Java并发包方面的问题。比如你对Java并发包的了解有多少?有了解是吧,那你在项目中使用过CountDownLatch吗?使用的场景有哪些?对它的原理知道多少?等等一系列问题。所以特意写一篇关于CountDownLatch的源码解读,帮助大家顺利通过面试。
比如说,在多线程情况下,其中一个或多个线程需要等待其他线程执行完毕才能执行。
比如执行A业务需要先查B业务和C业务,获取到B和C的返回值才能执行A业务,就可以使用到CountDownLatch,虽然也可以使用到传统的join关键字来实现。
/**
* @Author liha
* @Date 2022-02-20 20:49
* 李哈YYDS
*/
public class CountDownLatchTest {
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(()->{
System.out.println("此线程模拟A业务");
try {
// 开始等待B和C业务执行完毕
countDownLatch.await();
// A业务开始执行
TimeUnit.SECONDS.sleep(1);
System.out.println("A业务执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
System.out.println("此线程模拟B业务");
try {
// 模拟B业务的执行时间
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B业务执行完毕");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("此线程模拟C业务");
try {
// 模拟C业务的执行时间
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C业务执行完毕");
countDownLatch.countDown();
}).start();
}
}
如果传统的单线程情况下先查B再查C,要花费4秒,使用到多线程+CountDownLatch只需要花费3秒,如果业务更多的话效率会更高,并且建议一定要有一个全局的线程池来管理线程。
先查看到CountDownLatch的一个内部结构
可以看到也是内部维护了一个Sync对象,Sync对象继承AQS。 所以也是一个传统基于AQS框架的一个技术。并且与ReentrantLock那些技术相比内部方法比较少,比较简答。
// 内部构造方法,new一个Sync传一个计数器值进去。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 给AQS内部维护的state传值。
Sync(int count) {
setState(count);
}
对于AQS做一个解释吧,考虑可能有新手
AQS算是Java并发包下的一个框架,很多并发技术都是基于AQS来实现。
AQS内部维护了一个state,一个队列,一个当前线程的引用。
state:因为有些情况是可重入锁所以使用控制state来判断是否还有线程占用锁,内部使用volition和CAS来保证state的原子性。
队列:存放获取锁失败或阻塞的线程。内部是一个双向链表,往往head节点是一个sentinel节点,算是一个标识节点,作用是用来唤醒sentinel的next节点的线程。
await方法:线程休眠,当state为0被唤醒
countDown方法:计数器-1,也就是state减1,当state为0时,执行唤醒被休眠的线程。
先看到await()方法。
构造方法将计数器的值赋值给state,所以这里肯定是-1,进入到doAcquireSharedInterruptibly()方法中
// 首先我们先要明白await()方法多个线程都能使用,目的是休眠线程,等待计数器为0
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// Node.SHARED 代表共享节点,共享节点就代表当线程被唤醒时会唤醒下一个等待的共享节点。
// 将当前线程节点加入到AQS内部维护的队列中,如果内部队列还未初始化就执行初始化操作
// 具体的添加节点或者初始化操作方法看最下面的代码
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 开始自旋
for (;;) {
// 获取到当前线程节点的prev节点,也就是当前节点的上一个节点
final Node p = node.predecessor();
// 如果上一个节点是sentinel节点,因为head是指向于sentinel节点的,就达标可以尝试获取锁
if (p == head) {
// 尝试获取到锁,前面截图过这个方法的实现,也就是判断state的值是不是0。
int r = tryAcquireShared(arg);
// state如果不为0就为-1,所以进不了这边,如果为0了就是计数器已经都释放了。
if (r >= 0) {
// 将head指向于当前线程节点,并且获取到当前线程节点的next节点,也就是下一个节点
// 如果下一个节点是shared节点就唤醒。
setHeadAndPropagate(node, r);
// 将上一个节点的next指向于null,此时head也不指向于他,所以已经被丢弃出去。
p.next = null; // help GC
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire()将上一个节点的状态位改成-1
// parkAndCheckInterrupt()将当前线程park休眠让出cpu的占用。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
// 创建出当前节点,并且当前节点是共享节点
Node node = new Node(Thread.currentThread(), mode);
// tail是aqs外部队列指向队列最后一位的全局变量,未初始化的情况下肯定为null,已经初始化的话就获取到最后一位。
Node pred = tail;
if (pred != null) {
// 因为队列是一个双向链表,所以prev就把当前节点的上一个节点指向最后一个节点,也就是插入到最后一位
node.prev = pred;
// 把aqs尾部的tail全局变量指向到当前节点,也就是代表当前节点是最后一位。
if (compareAndSetTail(pred, node)) {
// 此时的pred是之前的最后一位的引用,所以把当前节点的前一位的next节点指向到当前节点
pred.next = node;
return node;
}
}
// 初始化操作。
// 不追进去了,这里大概讲解一下。
// 创建一个sentinel节点,将sentinel节点的waitState状态改为-1,并且将AQS维护的head全局变量
// 指向sentinel节点,并且将sentinel节点的next指向到当前节点,当前节点的prev指向到sentinel节点
// 其实也就是一个很普通的双向链表操作。
enq(node);
return node;
}
doAcquireSharedInterruptibly()方法中,将当前线程进入到park之前还是尝试了几次获取到锁,如果几次尝试并没有成功,计数器并没有减到0,就会进入到休眠状态,让出对cpu占用。
目前内部的队列的结构如下
再看到计数器减一的方法
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
先看到if判断中的tryReleaseShared()方法。
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 获取到AQS内部维护的state状态位,state状态位是CountDownLatch构造方法传进去的。
int c = getState();
// 注意这里是cas+死循环,所以是多线程来改变state状态位,所以可能抢不到的线程要自旋来该状态
// 并且当状态位为0的时候就代表CountDownLatch的计数器已经为0了,就达标其他线程通过下面的
// cas操作将state改成为0执行其他语句去了。 所以没抢到线程这里就已经没事可干了。
if (c == 0)
return false;
// 状态位预减1,注意这里是预减,因为多线程的情况下还要cas来保证原子性。
int nextc = c-1;
//cas操作,c是预期值,nextc是改变值,如果在并发的情况下为false的就继续自旋,为true的就判断是否状态为0,为0就要执行唤醒等待线程
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
当state状态位为0的时候就执行到doReleaseShared()方法。
// 能进来
private void doReleaseShared() {
// 自旋
for (;;) {
// 获取到head的指向节点,head也有可能为null,因为并没有线程在await
Node h = head;
// 判断是否有线程在await
if (h != null && h != tail) {
// 能进来的话,就代表head和tail有指向,并且head指向的sentinel节点
int ws = h.waitStatus;
// 在park之前将sentinel节点的waitStatus改为SIGNAL了
if (ws == Node.SIGNAL) {
// 这里考虑为什么要cas?
// 因为来await中的操作,显示初始化head节点,和sentinel节点之类的
// 初始化后才通过cas将sentinel节点的waitState改成SIGNAL
// 如果失败就自旋重试。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 这里是上面的cas操作成功了,就唤醒head指向的节点的next节点。
unparkSuccessor(h);
}
// PROPAGATE这个标志的修改我翻阅了帖子说是解决bug,但是全局就这里出现了。
// 后面等我了解了bug以后会写篇帖子来讲解。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 这里是多线程情况下预防await()方法对head和tail和队列初始化了。
if (h == head) // loop if head changed
break;
}
}
这里唤醒以后会进入到唤醒线程的park()方法处。
被唤醒后就查看是否需要继续唤醒当前节点的next节点。并且对链表和head和tail做一些处理。入地就是让当前节点的prev节点也就是sentinel节点指向为null,被gc回收。
被唤醒后就可以正常执行当前线程的业务逻辑了。
总体来说还是比较简单,建议大家debug来调试追一下,结合博主的帖子中代码中的注释来理解。
最后如果本帖对大家有帮助,希望大家点赞+关注!后续一直会出各种源码的解读!