java countdownlatch
1.背景:
-
countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue
-
存在于java.util.cucurrent包下
2.概念
-
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行
-
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了
-
只能一次性使用(不能reset);主线程阻塞;某个线程中断将永远到不了屏障点,所有线程都会一直等待
3.源码
countDownLatch类中只提供了一个构造器:
//参数count为计数值 public CountDownLatch(int count) { };
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await() throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() { };
3.1 源码分析
await() 源码解析:
await()
源码如下:
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
调用的是AQS
的acquireSharedInterruptibly(int arg)
方法:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 如果被中断,抛出异常 if (Thread.interrupted()) throw new InterruptedException(); // 尝试获取同步状态 if (tryAcquireShared(arg) < 0) // 获取同步状态失败,自旋 doAcquireSharedInterruptibly(arg); }
首先,通过tryAcquireShared(arg)
尝试获取同步状态,具体的实现被Sync
重写了,查看源码:
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
如果同步状态的值为0,获取成功。这就是CountDownLatch的机制,尝试获取latch的线程只有当latch的值减到0的时候,才能获取成功。如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 将当前线程加入同步队列的尾部 final Node node = addWaiter(Node.SHARED); try { // 自旋 for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); // 如果前驱节点是头结点,则尝试获取同步状态 if (p == head) { // 当前节点尝试获取同步状态 int r = tryAcquireShared(arg); if (r >= 0) { // 如果获取成功,则设置当前节点为头结点 setHeadAndPropagate(node, r); p.next = null; // help GC return; } } // 如果当前节点的前驱不是头结点,尝试挂起当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
这里,调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
挂起当前线程
countDown() 源码解析:
countDown()
源码如下:
public void countDown() { sync.releaseShared(1); }
调用的是AQS
的releaseShared(int arg)
方法:
public final boolean releaseShared(int arg) { // 尝试释放同步状态 if (tryReleaseShared(arg)) { // 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点 doReleaseShared(); return true; } return false; }
首先,通过tryReleaseShared(arg)
尝试释放同步状态,具体的实现被Sync
重写了,源码:
protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; // 同步状态值减1 int nextc = c - 1; if (compareAndSetState(c, nextc)) return nextc == 0; } }
如果同步状态值减到0
,则释放成功,进入自旋,尝试唤醒同步队列中头结点的后继节点,调用的是AQS
的doReleaseShared()
函数:
private void doReleaseShared() { for (;;) { // 获取头结点 Node h = head; if (h != null && h != tail) { // 获取头结点的状态 int ws = h.waitStatus; // 如果是SIGNAL,尝试唤醒后继节点 if (ws == Node.SIGNAL) { if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒头结点的后继节点 unparkSuccessor(h); } else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
这里调用了unparkSuccessor(h)
去唤醒头结点的后继节点
如何唤醒所有调用 await() 等待的线程:
回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)
方法中:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 将当前线程加入同步队列的尾部 final Node node = addWaiter(Node.SHARED); try { // 自旋 for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); // 如果前驱节点是头结点,则尝试获取同步状态 if (p == head) { // 当前节点尝试获取同步状态 int r = tryAcquireShared(arg); if (r >= 0) { // 如果获取成功,则设置当前节点为头结点 setHeadAndPropagate(node, r); p.next = null; // help GC return; } } // 如果当前节点的前驱不是头结点,尝试挂起当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } catch (Throwable t) { cancelAcquire(node); throw t; } }
该方法里面,通过调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()将线程挂起。当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。查看源码:
// 如果执行这个函数,那么propagate一定等于1 private void setHeadAndPropagate(Node node, int propagate) { // 获取头结点 Node h = head; // 因为当前节点被唤醒,设置当前节点为头结点 setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 获取当前节点的下一个节点 Node s = node.next; // 如果下一个节点为null或者节点为shared节点 if (s == null || s.isShared()) doReleaseShared(); } }
这里,当前节点被唤醒,首先设置当前节点为头结点。如果当前节点的下一个节点是shared
节点,调用doReleaseShared()
,源码:
private void doReleaseShared() { // 自旋 for (;;) { // 获取头结点,也就是当前节点 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果head没有改变,则调用break退出循环 if (h == head) break; } }
首先,注意if (h == head) break; 这里每次循环的时候判断head头结点有没有改变,如果没有改变则退出循环。因为只有当新的节点被唤醒之后,新节点才会调用setHead(node)设置自己为头结点,头结点才会改变。其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)
如果队列中有两个或以上个节点,那么检查局部变量h的状态:
如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒
如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到
如果状态为PROPAGATE,直接判断head是否变化
4.例子
//创建初始化3个线程的线程池 private ExecutorService threadPool = Executors.newFixedThreadPool(3); //保存每个学生的平均成绩 private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); private CountDownLatch countDownLatch = new CountDownLatch(3); private void count() { for (int i = 0; i < 3; i++) { threadPool.execute(() -> { //计算每个学生的平均成绩,代码略()假设为60~100的随机数 int score = (int) (Math.random() * 40 + 60); try { Thread.sleep(Math.round(Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } map.put(Thread.currentThread().getName(), score); System.out.println(Thread.currentThread().getName() + "同学的平均成绩为" + score); countDownLatch.countDown(); }); } this.run(); threadPool.shutdown(); } @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } int result = 0; Set<String> set = map.keySet(); for (String s : set) { result += map.get(s); } System.out.println("三人平均成绩为:" + (result / 3) + "分"); } public static void main(String[] args) throws InterruptedException { long now = System.currentTimeMillis(); CyclicBarrierTest cb = new CyclicBarrierTest(); cb.count(); Thread.sleep(100); long end = System.currentTimeMillis(); System.out.println("消耗时间(毫秒):" + (end - now)); }
1. CountDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
2. CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用
4. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行
对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待
CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!