CountDownLatch 原理

一、CountDownLatch 原理


1、CountDownLatch在多线程并发编程中充当一个计时器的功能,并且内部维护一个count的变量,并且其操作都是原子操作,该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。


2、如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。比如下面的例子就是有两个操作,一个是读操作一个是写操作,现在规定必须进行完写操作才能进行读操作。所以当最开始调用读操作时,需要用await()方法使其阻塞,当写操作结束时,则需要使count等于0。因此count的初始值可以定为写操作的记录数,这样便可以使得进行完写操作,然后进行读操作。


二、构造方法:CountDownLatch

内部也是有个Sync类继承了AQS,所以CountDownLatch类的构造方法就是调用Sync类的构造方法,然后调用setState()方法设置AQS中state的值


三、方法:countDown()

这个方法会对state值减1,会调用到AQS中releaseShared()方法,目的是为了调用doReleaseShared()方法,这个是AQS定义好的释放资源的方法,而tryReleaseShared()则是子类实现的,可以看到是一个自旋CAS操作,每次都获取state值,如果为0则直接返回,否则就执行减1的操作,失败了就重试,如果减完后值为0就表示要释放所有阻塞住的线程了,也就会执行到AQS中的doReleaseShared()方法


1、线程进入 countDown() 完成计数器减一(释放锁)的操作

  public void countDown() {
      sync.releaseShared(1);
  }
  public final boolean releaseShared(int arg) {
      // 尝试释放共享锁
      if (tryReleaseShared(arg)) {
          // 释放锁成功开始唤醒阻塞节点
          doReleaseShared();
          return true;
      }
      return false;
  }


2、更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true

  protected boolean tryReleaseShared(int releases) {
      for (;;) {
          int c = getState();
          // 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false
          if (c == 0)
              return false;
          // 计数器减一
          int nextc = c-1;
          if (compareAndSetState(c, nextc))
              // 计数器为 0 时返回 true
              return nextc == 0;
      }
  }
  • a、获取当前状态:调用 getState() 获取当前计数器的值。

  • b、检查计数器是否为零:如果计数器已经为零,则返回 false,表示不需要唤醒等待的线程。

  • c、减少计数器:将计数器减一,得到新的计数器值 nextc。

  • d、CAS 操作:使用 compareAndSetState(c, nextc) 尝试将计数器的值从 c 更新为 nextc。如果成功,则返回 nextc == 0,表示计数器是否减到零。


3、state = 0 时,当前线程需要执行唤醒阻塞节点的任务

  private void doReleaseShared() {
      for (;;) {
          Node h = head;
          // 判断队列是否是空队列
          if (h != null && h != tail) {
              int ws = h.waitStatus;
              // 头节点的状态为 signal,说明后继节点没有被唤醒过
              if (ws == Node.SIGNAL) {
                  // cas 设置头节点的状态为 0,设置失败继续自旋
                  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                      continue;
                  // 唤醒后继节点
                  unparkSuccessor(h);
              }
              // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
              else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                  continue;
          }
          // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head,
          // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
          if (h == head)
              break;
      }
  }

小结:当线程使用 countDown() 方法时,其实是使用了 tryReleaseShared() 方法以 CAS 的操作来减少 state ,直至 state 为 0 ,进而释放锁资源,唤醒后续节点。

  • a、判断队列是否是空队列。

  • b、检查头节点的状态:如果头节点的状态为 Node.SIGNAL,则表示需要唤醒后续节点。

  • c、CAS 操作:使用 compareAndSetWaitStatus(h, Node.SIGNAL, 0) 尝试将头节点的状态从 Node.SIGNAL 更新为 0。如果成功,则调用 unparkSuccessor(h) 唤醒后续节点。

  • d、检查头节点是否变化:如果头节点没有变化,则退出循环。


在开始分析 doReleaseShared() 之前,我们先来补全一下 AQS 中 waitStatus 的状态说明

  • 初始化状态:0,表示当前节点在同步队列中,等待获取锁;

  • CANCELLED:1,表示当前节点取消获取锁;

  • SIGNAL:-1,表示后续节点等待当前节点唤醒;

  • CONDITION:-2,表示当前线程正在条件等待队列中;

  • PROPAGATE:-3,共享模式,前置节点唤醒后续节点后,唤醒操作无条件传播下去;


四、方法:await()


1、线程调用 await() 等待其他线程完成任务:支持打断

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }


2、AbstractQueuedSynchronizer#acquireSharedInterruptibly方法

    public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        // 判断线程是否被打断,抛出打断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源
        // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
  • a、检查中断状态:首先检查当前线程是否被中断,如果被中断则抛出 InterruptedException。

  • b、尝试获取共享锁:调用 tryAcquireShared(arg) 方法尝试获取共享锁。在 CountDownLatch 中,tryAcquireShared 的实现是检查计数器是否为零,如果为零则返回 1,否则返回 -1。

  • c、如果获取锁失败:如果 tryAcquireShared 返回 -1,表示当前计数器不为零,调用 doAcquireSharedInterruptibly(arg) 方法将当前线程加入等待队列并阻塞。


3、线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0:

    private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
        // 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                // 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 前驱节点时头节点就可以尝试获取锁
                if (p == head) {
                    // 再次尝试获取锁,获取成功返回 1
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 获取锁成功,设置当前节点为 head 节点,并且向后传播
                        setHeadAndPropagate(node, r);
                        p.next = null; // 到了这步时p节点会被删除
                        failed = false;
                        return;
                    }
                }
                // parkAndCheckInterrupt阻塞在这里
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            // 阻塞线程被中断后抛出异常,进入取消节点的逻辑
            if (failed)
                cancelAcquire(node);
        }
    }
  • a、将当前线程加入等待队列:调用 addWaiter(Node.SHARED) 将当前线程包装成一个 Node 节点并加入等待队列。哨兵节点--->Node节点

  • b、自旋等待:在一个无限循环中,检查当前节点的前驱节点是否是头节点。如果是头节点,则尝试获取共享锁。

  • c、获取锁成功:如果 tryAcquireShared(arg) 返回大于等于 0 的值,表示获取锁成功,调用 setHeadAndPropagate(node, r) 设置头节点并唤醒后续的共享节点。

  • d、检查是否需要阻塞:如果获取锁失败,调用 shouldParkAfterFailedAcquire(p, node) 检查是否需要阻塞当前线程。如果需要阻塞,则调用 parkAndCheckInterrupt() 阻塞当前线程并检查中断状态。

  • e、处理中断:如果在阻塞过程中线程被中断,则抛出 InterruptedException。


4、获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点:

  private void setHeadAndPropagate(Node node, int propagate) {
      Node h = head;
      // 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null
      setHead(node);
  	// propagate = 1,条件一成立
      if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
          // 获取当前节点的后继节点
          Node s = node.next;
          // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式
          if (s == null || s.isShared())
              // 唤醒所有的等待共享锁的节点
              doReleaseShared();
      }
  }
  • a、将当前的头节点 head 赋值给局部变量 h,以便后续检查使用

  • b、将传入的 node 设置为新的头节点 head

  • c、这个条件判断包含了多个子条件,用于决定是否进行进一步的操作:

    • propagate > 0:如果 propagate 大于 0,说明state已经为0,表示需要传播信号。

    • h == null:如果旧的头节点为空,表示链表或队列之前是空的。

    • h.waitStatus < 0:如果旧的头节点的状态小于 0,表示该节点可能处于某种特殊状态(例如等待唤醒)。

    • (h = head) == null:再次检查新的头节点是否为空。

    • h.waitStatus < 0:再次检查新的头节点的状态。

  • d、当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式

小结:当线程使用 await() 方法时会将当前线程封装成 node 加入AQS 队列中,如果发现 state 不为0,说明还有任务未执行完成,继续阻塞;如果 state 为0,会释放掉所有的等待线程,执行 await() 之后的数据。


5、doReleaseShared方法,唤醒队列中阻塞节点

private void doReleaseShared() {
    for (;;) {

        // 此时head已经为传入Node,因为在前面已经重置了
        Node h = head;
        // 判断队列是否是空队列
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 头节点的状态为 signal,说明后继节点没有被唤醒过
            if (ws == Node.SIGNAL) {
                // cas 设置头节点的状态为 0,设置失败继续自旋
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head,
        // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
        if (h == head)
            break;
    }
}


6、进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点

//该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

	   //获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。
	   int ws = pred.waitStatus;

	   // 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回true
	   if (ws == Node.SIGNAL)
		  return true;

       // 如果上一个节点的状态大于0,说明已经失效了
       if (ws > 0) {
			 do {
			 
				// 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给 pred
				node.prev = pred = pred.prev;
			 
			 } while (pred.waitStatus > 0); // 一直找到小于等于0的
			
			 // 将重新标识好的最近的有效节点的 next 指向当前节点
			 pred.next = node;
		 
       } else {
	   
		  // 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
       }
       return false;
 }
  • a、shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)

  • b、当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true

  • c、进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)


7、parkAndCheckInterrupt() 阻塞当前线程并检查中断状态

private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
    LockSupport.park(this);
    // 判断当前线程是否被打断,清除打断标记
    return Thread.interrupted();
}


案例:

    package com.item;

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;

    public class CountDownLatchtTest {

        private static final int N = 10; // 乘客数


        public static void main(String[] args) throws InterruptedException {

            //step1:创建倒数闩,设置倒数的总数
            CountDownLatch doneSignal = new CountDownLatch(N);

            Executor e = Executors.newFixedThreadPool(N);

            for (int i = 1; i <= N; ++i) {
                e.execute(new Person1(doneSignal, i));
            }
            doneSignal.await(); //step2:等待报数完成,倒数闩计数值为0
            System.out.println("人到齐,开车");
        }
    }

    class Person1 implements Runnable{

        private final CountDownLatch doneSignal;

        private final int i;

        Person1(CountDownLatch doneSignal, int i) {
            this.doneSignal = doneSignal;
            this.i = i;
        }

        @Override
        public void run() {
            //报数
            System.out.println("第" + i + "个人已到");
            doneSignal.countDown(); //step3:倒数闩减少1

        }

    }


五、流程图了解一下


六、CountDownLatch的文字描述


假设 CountDownLatch 初始计数器为 3(state = 3),以下是线程并发操作的动态过程:


步骤 1:初始化

  • 状态:state = 3,CLH 队列为空。

  • 线程 T1、T2、T3:调用 countDown() 减少计数器。

  • 线程 W1、W2:调用 await() 等待计数器归零。


步骤 2:线程 W1 调用 await()

  • a、检查计数器:state = 3 > 0,线程 W1 加入 CLH 队列并阻塞。


步骤 3:线程 T1 调用 countDown()

  • a、减少计数器:state = 3 → 2

  • b、未触发唤醒:state > 0,不唤醒队列中的线程


步骤 4:线程 W2 调用 await()

  • a、检查计数器:state = 2 > 0,线程 W2 加入 CLH 队列并阻塞。


步骤 5:线程 T2 调用 countDown()

  • a、减少计数器:state = 2 → 1

  • b、未触发唤醒:state > 0,不唤醒队列中的线程


步骤 6:线程 T3 调用 countDown()

  • a、减少计数器:state = 1 → 0

  • b、触发唤醒:state == 0,唤醒 CLH 队列中的所有线程

  • c、线程 W1、W2 被唤醒:继续执行后续逻辑

posted @ 2024-06-21 21:04  jock_javaEE  阅读(125)  评论(0)    收藏  举报