AQS共享模式之CountDownLatch源码解析

1.简单使用

1.1主线程等待分线程完成任务

public class CountDownLatchTest01 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch latch = new CountDownLatch(8);
		
        /*
         *这里为了方便测试直接使用Executors工具类创建了线程池,注意实战尽量不使用这种方式创建线程池
         */
        ExecutorService threadPool = Executors.newFixedThreadPool(8);
		
        //执行8个任务
        for (int i = 0; i < 8; i++) {
    
    
            final int k = i;
            threadPool.execute(() -> {
    
    
                System.out.println("线程 " + k + " 启动");
                latch.countDown(); //执行一次countDown  内部的值-1
            });
        }
        
        latch.await(); //阻塞等待 任务执行完毕latch内部的值变为0 才会继续向下执行
        System.out.println("任务执行完毕 !!!");
    }
}

1.2主线程与分线程相互控制

public class CountDownLatchTest02 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch mainLatch = new CountDownLatch(1);
        CountDownLatch divisionLatch = new CountDownLatch(8);
	
        ExecutorService threadPool = Executors.newFixedThreadPool(8);

        for (int i = 0; i < 8; i++) {
    
    
            final int k = i;
            threadPool.execute(() -> {
    
    
                try {
    
    
                    //先阻塞住 等待mainLatch内部值变为0
                    mainLatch.await();
                    System.out.println("线程 " + k + " 启动");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    //divisionLatch内部值-1
                    divisionLatch.countDown();
                }
            });
        }
		
        TimeUnit.SECONDS.sleep(3);
        System.out.println("mainLatch栅栏打开");
        mainLatch.countDown(); //打开栅栏 分线程开始执行任务

        System.out.println("等待divisionLatch栅栏打开 。。。");
        //阻塞 等待分线程任务执行完毕才能继续向下执行
        divisionLatch.await();
        System.out.println("divisionLatch栅栏已打开。。。继续向下执行");
    }
}

//执行结果
/*
mainLatch栅栏打开 //主线程控制分线程的开始
等待divisionLatch栅栏打开 。。。
线程 6 启动divisionLatch - 1 = 5
线程 1 启动divisionLatch - 1 = 0
线程 3 启动divisionLatch - 1 = 2
线程 4 启动divisionLatch - 1 = 3
线程 8 启动divisionLatch - 1 = 7
线程 7 启动divisionLatch - 1 = 6
线程 5 启动divisionLatch - 1 = 4
线程 2 启动divisionLatch - 1 = 1
//分线程控制主线程继续向下执行
divisionLatch栅栏已打开。。。继续向下执行
*/

1.3CountDownLatch运行示意图

在这里插入图片描述

2.源码解析

await大致流程图

在这里插入图片描述

signal大致流程图

在这里插入图片描述

内部核心结构分析

核心使用的就是AQS的共享模式,核心的内部类Sync就是继承的AQS。关于AQS的源码解析

参考

private static final class Sync extends AbstractQueuedSynchronizer {
    
    
}

2.1await()方法

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

	public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    
    
        //线程处于中断状态,直接抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        
        /*
         *   tryAcquireShared(arg)方法 
         *  # 返回值 < 0 表示state值为0 下面需要入同步队列
         *  # 返回值 > 0 表示state值不为0 此时就不会阻塞线程了,,,对应业务层面执行业务
         *   调用await()的线程就会被唤醒
         */
        if (tryAcquireShared(arg) < 0)
            //返回值小于0,说明此时state > 0,需要阻塞当前线程。
            doAcquireSharedInterruptibly(arg);
    }

2.2tryAcquireShared()

protected int tryAcquireShared(int acquires) {
    
    
        return (getState() == 0) ? 1 : -1;
}

2.3AQS#doAcquireInterruptibly()

   private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    
    
       
        //将当前线程封装成一个Node,模式为共享模式,并进入同步队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
    
    
            for (;;) {
    
    
                //获取当前节点的前驱节点
                final Node p = node.predecessor();
                //p == head 说明当前线程对应的节点是head.next节点,有权利获取共享锁了
                if (p == head) {
    
    
                    int r = tryAcquireShared(arg);
           // r >= 0说明当前state = 0 前驱节点已经释放锁了,可以唤醒当前node了。
                    if (r >= 0) {
    
    
                        setHeadAndPropagate(node, r);
                        p.next = null; 
                        failed = false;
                        return;
                    }
                }
                /*
                 * shouldParkAfterFailedAcquire()会在队列中找到一个状态正常的node,
                 * 然后将这个node的后继指向当前线程对应的节点,然后将前驱节点的状态设置 
                 * 为-1(signal)。 详细讲解见AQS源码解析
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //挂起当前线程。
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
    }

2.4AQS#setHeadAndPropagate()

    /*
     *  设置当前节点为head节点,并且向后传播(依次唤醒)
     *  @param node  
     *  @param propagate 这个值一定是1
     */	
  
	private void setHeadAndPropagate(Node node, int propagate) {
    
    
        Node h = head; 
        //将当前节点设置为新的node节点
        setHead(node);
        
		//propagate一定是1
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            //获取后继节点
            Node s = node.next;
            
            /*
             * 条件一:
             * s == null 当前node已经是tail了,条件一定成立,doReleaseShared()会处理
             * 
             * 条件二:
             * 前置条件 s != null,并且s是共享节点,调用await()时构造节点时就是共享模式了
             * 基本上所有的情况都会执行到 doReleaseShared() 
             */
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

2.4countDown()

public void countDown() {
    
    
        sync.releaseShared(1);
}

2.5AQS#releaseShared()

    public final boolean releaseShared(int arg) {
    
    
        /*
         *  条件成立: 说明当前调用latch.countDown()方法的线程 正好是 state - 1 = 0 
         *  的线程,需要做唤醒await()阻塞线程的逻辑
         */ 
        if (tryReleaseShared(arg)) {
    
    
            /*
             * 进入if的线程说明当前线程就是将state - 1后变为0的线程,此时需要
             * 调用doReleaseShared()唤醒阻塞状态的线程。
             */
            doReleaseShared();
            return true;
        }
        return false;
    }

2.6tryReleaseShared

    /*
     *  自旋 + CAS更新state 每次将state的值 - 1。
     *  @return true -> state = 0, false -> state != 0
     */   
	protected boolean tryReleaseShared(int releases) {
    
    
            for (;;) {
    
    
                int c = getState(); 
                if (c == 0)
                    return false;
                int nextc = c - 1;
                //CAS更新state。将state - 1
                if (compareAndSetState(c, nextc))
                    //CAS更新后 判断nextc是否等于0
                    return nextc == 0;
            }
        }
    }

2.7doReleaseShared()

/**
  * 都有哪几种路径会调用到doReleaseShared方法呢?
  * 1.latch.countDown() -> AQS.state == 0 -> doReleaseShared() 唤醒当前阻塞队列内的 head.next 对应的线程。
  * 2.被唤醒的线程 -> doAcquireSharedInterruptibly parkAndCheckInterrupt() 唤醒 -> setHeadAndPropagate() -> doReleaseShared()
  */
// AQS.doReleaseShared
private void doReleaseShared() {
    
    
    for (;;) {
    
    
        // 获取当前AQS 内的 头结点
        Node h = head;
        // 条件一:h != null 成立,说明阻塞队列不为空..
        // 不成立:h == null 什么时候会是这样呢?
        // latch创建出来后,没有任何线程调用过 await() 方法之前,有线程调用latch.countDown()操作 且触发了 唤醒阻塞节点的逻辑..

        // 条件二:h != tail 成立,说明当前阻塞队列内,除了head节点以外  还有其他节点。
        // h == tail  -> head 和 tail 指向的是同一个node对象。 什么时候会有这种情况呢?
        // 1. 正常唤醒情况下,依次获取到 共享锁,当前线程执行到这里时 (这个线程就是 tail 节点。)
        // 2. 第一个调用await()方法的线程 与 调用countDown()且触发唤醒阻塞节点的线程 出现并发了..
        //   因为await()线程是第一个调用 latch.await()的线程,此时队列内什么也没有,它需要补充创建一个Head节点,然后再次自旋时入队
        //   在await()线程入队完成之前,假设当前队列内 只有 刚刚补充创建的空元素 head 。
        //   同期,外部有一个调用countDown()的线程,将state 值从1,修改为0了,那么这个线程需要做 唤醒 阻塞队列内元素的逻辑..
        //   注意:调用await()的线程 因为完全入队完成之后,再次回到上层方法 doAcquireSharedInterruptibly 会进入到自旋中,
        //   获取当前元素的前驱,判断自己是head.next, 所以接下来该线程又会将自己设置为 head,然后该线程就从await()方法返回了...
        if (h != null && h != tail) {
    
    
            // 执行到if里面,说明当前head 一定有 后继节点!

            int ws = h.waitStatus;
            // 当前head状态 为 signal 说明 后继节点并没有被唤醒过呢...
            if (ws == Node.SIGNAL) {
    
    
                // 唤醒后继节点前 将head节点的状态改为 0
                // 这里为什么,使用CAS呢? 回头说...
                // 当doReleaseShared方法 存在多个线程 唤醒 head.next 逻辑时,
                // CAS 可能会失败...
                // 案例:
                // t3 线程在if(h == head) 返回false时,t3 会继续自旋. 参与到 唤醒下一个head.next的逻辑..
                // t3 此时执行到 CAS WaitStatus(h,Node.SIGNAL, 0) 成功.. t4 在t3修改成功之前,也进入到 if (ws == Node.SIGNAL) 里面了
                // 但是t4 修改 CAS WaitStatus(h,Node.SIGNAL, 0) 会失败,因为 t3 改过了...
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后继节点
                unparkSuccessor(h);
            }

            else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }

        // 条件成立:
        // 1.说明刚刚唤醒的 后继节点,还没执行到 setHeadAndPropagate方法里面的 设置当前唤醒节点为head的逻辑。
        // 这个时候,当前线程 直接跳出去...结束了..
        // 此时用不用担心,唤醒逻辑 在这里断掉呢?、
        // 不需要担心,因为被唤醒的线程 早晚会执行到doReleaseShared方法。

        // 2.h == null  latch创建出来后,没有任何线程调用过 await() 方法之前,
        // 有线程调用latch.countDown()操作 且触发了 唤醒阻塞节点的逻辑..
        // 3.h == tail  -> head 和 tail 指向的是同一个node对象

        // 条件不成立:
        // 被唤醒的节点 非常积极,直接将自己设置为了新的head,此时 唤醒它的节点(前驱),执行h == head 条件会不成立..
        // 此时 head节点的前驱,不会跳出 doReleaseShared 方法,会继续唤醒 新head 节点的后继...
        if (h == head)                   // loop if head changed
            break;
    }
}

3.总结

  • CountDownLatch表示允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作
  • CountDownLatch使用AQS的共享锁机制实现;
  • CountDownLatch初始化的时候需要传入次数count;
  • 每次调用countDown()方法count的次数减1;
  • 每次调用await()方法的时候会尝试获取锁,这里的获取锁其实是检查AQS的state变量的值是否为0
  • 当count的值(也就是state的值)减为0的时候会唤醒排队着的线程(这些线程调用await()进入队列);

参考路飞大佬

おすすめ

転載: blog.csdn.net/qq_46312987/article/details/121754281