深入理解CountDownLatch

概念

CountDownLatch可以用来实现一个或者多个线程等待其他线程完成一组特定的操作之后才能继续运行。这组操作被称为先决操作

使用

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch countDownLatch=new CountDownLatch(2);

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//2-1
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//1-1=0
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    countDownLatch.await();//阻塞,count=0时唤醒
}

运行结果:

在这里插入图片描述
CountDownLatch内部会维护一个用于表示未完成的先决操作数量的计数器。CountDownLatch.countDown()每执行一次就会使相应实例的计数器值减少1。
CountDownLatch.await()是一个阻塞方法,当计数器值为0时,代表所有的先决操作已执行完毕,则唤醒阻塞的线程。

源码分析

注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。

先来看一下CountDownLatch类的主要方法
在这里插入图片描述
再来看一下它的类结构图
在这里插入图片描述
CountDownLatch类是一个独立的类,只有一个内部类Sync,这个类继承了 AQS 抽象类。之前我们说过,AQS 是 JUC 所有锁的实现,定义了锁的基本操作。
这里,我们主要分析await()countDown() 两个方法
先来看CountDownLatch的构造方法

构造方法

public CountDownLatch(int count) {
    
    
//count不能小于0 ,否则会抛异常
 if (count < 0) throw new IllegalArgumentException("count < 0");
 	//调用内部类的构造方法
    this.sync = new Sync(count);
}

内部类Sync的构造方法

 Sync(int count) {
    
    
 	//设置锁的state值
 	setState(count);
 }

直接调用可父类AQS的方法,设置锁的state值,之前我们讲过,通过控制这个变量,能够实现共享共享锁或者独占锁。

await()

public void await() throws InterruptedException {
    
    
  sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    
    
    //判断线程的中断标记,interrupted默认是false
    if (Thread.interrupted())
        throw new InterruptedException();
        //判断锁的state值,
        //等于0,需要把当前线程唤醒
    if (tryAcquireShared(arg) < 0)
    	//不等于0,需要去把当前线程挂起,阻塞
        doAcquireSharedInterruptibly(arg);
}

之前我们在分析独占锁的时候是通过tryAcquire(arg)这个方法去抢占锁的,而这次共享锁则通过tryAcquireShared(arg) 方法

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

点进去发现,它是去判断锁的state值,如果state为0,则表示将当前挂机的线程唤醒;如果不等于0,说明当前线程不能获得锁,需要调用doAcquireSharedInterruptibly(arg) 阻塞挂起

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException 
    //将当前线程的节点加到AQS队列的尾部
    //并返回这个节点,也是队列的最后一个节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
    
    
    	//自旋
        for (;;) {
    
    
        	//找到当前节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点是head,说明前面已经没有线程阻挡它获得锁
            if (p == head) {
    
    
            	//去获得锁的state值
                int r = tryAcquireShared(arg);
                //表示state值为0
                if (r >= 0) {
    
    
                	//将当前节点变为head节点
                	//开始传播,并且一个接一个将后面的线程都唤醒
                    setHeadAndPropagate(node, r);
                    //将当前节点踢出去,帮助gc清理
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //获取不到锁,就需要阻塞当前线程
            //阻塞之前,需要改变前一个节点的状态。如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
            //这是为了提醒前一个节点释放锁完后能够叫醒自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

先通过addWaiter方法将当前节点加到AQS队列的尾部。
当前一个节点是head节点,且锁的状态为0时,执行**setHeadAndPropagate()**方法去唤醒线程

private void setHeadAndPropagate(Node node, int propagate) {
    
    
		//将当前节点变为head节点
        Node h = head; // Record old head for check below
        setHead(node);
       
       	//一个一个传播,将后面的线程全部唤醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            Node s = node.next;
            //s==null是为了防止多线程操作时,再添加节点到为节点的时候,只来的及 node.prev = pred;
            //而 pred.next = node;还没执行的情况
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

这里需要说一下共享锁与独占锁在唤醒时的区别:

  • 独占锁:当锁被头节点获取后,只有头节点获取锁,其余节点的线程继续沉睡,等待锁被释放后,才会唤醒下一个节点的线程。
  • 共享锁:只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。

当前一个节点不是head节点,就需要阻塞等待当前线程,这里逻辑和ReentrantLock逻辑一样:

  • 如果当前节点的前一个节点是head节点,则可以去尝试获得锁;
  • 如果不是,则需要将前一个节点的状态变为SIGNAL ,然后自己阻塞等待。
    具体方法分析参考之前的文章深入理解ReentrantLock
    看了await方法,countDown方法的实现也就差不多了

countDown()

public void countDown() {
    
    
   sync.releaseShared(1);
  }
  
public final boolean releaseShared(int arg) {
    
    
//将state值减1
 if (tryReleaseShared(arg)) {
    
    
 		//当state=0时执行
 		//去唤醒之前阻塞等待的线程
        doReleaseShared();
        return true;
    }
    return false;
}

看一下CountDownLatch类对tryReleaseShared方法的实现

protected boolean tryReleaseShared(int releases) {
    
    
	//防止CAS失败,自旋
     for (;;) {
    
    
     	//获取锁的state
         int c = getState();
         if (c == 0)
             return false;
         int nextc = c-1;
         if (compareAndSetState(c, nextc))
             return nextc == 0;
     }
 }

逻辑很简单,获得锁的state值

  • 如果开始state的值就为0,则返回false;
  • 如果再减1之后为0,则返回true

当state减到0之后,就要去唤醒阻塞等待的线程

private void doReleaseShared() {
    
    
 for (;;) {
    
    
     Node h = head;
     if (h != null && h != tail) {
    
    
         int ws = h.waitStatus;
         if (ws == Node.SIGNAL) {
    
    
             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
     }
     if (h == head)                   // loop if head changed
         break;
 }
}

只要队列中 head 节点不是 null,且和 tail 不相等,并且状态是 SIGNAL,通过 CAS 将状态修改成 0,如果成功,唤醒当前线程。当前线程就会在 doAcquireSharedInterruptibly 方法中苏醒,再次尝试获取锁,获得锁后,继续唤醒下一个线程,就像多米诺骨牌效应一样。

总结

  • CountDownLatch内部计数器值达到0之后其值就恒定不变,后面继续执行await方法的任何一个线程都不会被暂停
  • 为了避免等待线程永远被暂停,countDown() 方法必须放在代码总是可以被执行到的地方,比如finally块中

猜你喜欢

转载自blog.csdn.net/xzw12138/article/details/106501919