01

概述

1 CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。 2 是通过一个计数器来实现的,计数器的初始值是线程的数量。 3 每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

02

源码解析

01 构造函数 当我们调用CountDownLatch countDownLatch=new CountDownLatch(4) 时候,此时会创建一个AQS的同步队列,并把创建CountDownLatch 传进来的计数器赋值给AQS队列的 state,所以state的值也代表CountDownLatch所剩余的计数次数

//参数count为计数值
public CountDownLatch(int count) {
   if (count < 0) throw new IllegalArgumentException("count < 0");
   this.sync = new Sync(count);//创建同步队列,并设置初始计数器值
}

02 await() 创建一个节点,加入到AQS阻塞队列,并同时把当前线程挂起。

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

03 acquireSharedInterruptibly(int arg)

//1.判断当前线程是否有被中断
//2.如果没有的话,就调用tryAcquireShared(int acquires)方法,判断当前线程是否还需要“阻塞”。
public final void acquireSharedInterruptibly(int arg)
           throws InterruptedException {
       if (Thread.interrupted())
           throw new InterruptedException();
       if (tryAcquireShared(arg) < 0)
           doAcquireSharedInterruptibly(arg);
}

04 doAcquireSharedInterruptibly(int arg)

private void doAcquireSharedInterruptibly(int arg)
       throws InterruptedException {
    //加入等待队列           
       final Node node = addWaiter(Node.SHARED);
       boolean failed = true;
    // 进入 CAS 循环
       try {
           for (;;) {
               //当一个节点(关联一个线程)进入等待队列后, 获取此节点的 prev 节点
               final Node p = node.predecessor();
               // 如果获取到的 prev 是 head,也就是队列中第一个等待线程
               if (p == head) {
                   // 再次尝试申请 反应到 CountDownLatch 就是查看是否还有线程需要等待(state是否为0)
                   int r = tryAcquireShared(arg);
                   // 如果 r >=0 说明 没有线程需要等待了 state==0
                   if (r >= 0) {
                       //尝试将第一个线程关联的节点设置为 head
                       setHeadAndPropagate(node, r);
                       p.next = null; // help GC
                       failed = false;
                       return;
                   }
               }
               //经过自旋tryAcquireShared后,state还不为0,就会到这里,第一次的时候,waitStatus是0,那么node的waitStatus就会被置为SIGNAL,第二次再走到这里,就会用LockSupport的park方法把当前线程阻塞住
               //重组双向链表,清空无效节点,挂起当前线程
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   throw new InterruptedException();
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
}

05 总结 1 当前线程就会进入了一个死循环当中,在这个死循环里面,会不断的进行判断 2 通过调用tryAcquireShared方法,不断判断我们上面说的那个计数器, 3 看看它的值是否为0了(为0的时候,其实就是我们调用了足够多次数的countDownLatch.countDown()方法的时候) 4 如果是为0的话,tryAcquireShared就会返回1,设置第一个线程关联的借点未head,然后跳出了循环,不再“阻塞”当前线程了。 5 需要注意的是,说是在不停的循环,其实也并非在不停的执行for循环里面的内容,因为在后面调用parkAndCheckInterrupt()方法时,在这个方法里面是会调用 LockSupport.park(this)来禁用当前线程的。

06 parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
   return Thread.interrupted();
}

07 addWaiter(Node mode) 将当前线程加入等待队列

static final Node SHARED = new Node();

static final Node EXCLUSIVE = null;

private Node addWaiter(Node mode) {
       Node node = new Node(Thread.currentThread(), mode);
       // 尝试快速入队操作,因为大多数时候尾节点不为 null
       Node pred = tail;
       if (pred != null) {
           node.prev = pred;
           if (compareAndSetTail(pred, node)) {
               pred.next = node;
               return node;
           }
       }
    //如果尾节点为空(也就是队列为空) 或者尝试CAS入队失败(由于并发原因),进入enq方法
       enq(node);
       return node;
}

步骤 1.构造Node实体,参数为当前线程和mode,mode是SHARED 或者 EXCLUSIVE 2.尝试快速入队列操作 3.如果尾节点为空(也就是队列为空) 或者尝试CAS入队失败(由于并发原因),进入enq方法

08 enq(final Node node)

private Node enq(final Node node) {
    // 死循环+CAS保证所有节点都入队
       for (;;) {
           Node t = tail;
           // 如果队列为空 设置一个空节点作为 head
           if (t == null) { // Must initialize
               if (compareAndSetHead(new Node()))
                   tail = head;
           } else {
               //加入队尾
               node.prev = t;
               if (compareAndSetTail(t, node)) {
                   t.next = node;
                   return t;
               }
           }
       }
}

说明 1.死循环+CAS 乐观锁 2.CAS是实现原子性,compareAndSetTail底层调用的是unsafe类,直接操作内存,在cpu层上加锁,直接对内存进行操作

09 setHeadAndPropagate(node, r)

private void setHeadAndPropagate(Node node, int propagate) {
   //备份head
   Node h = head;
   //抢到锁的线程被唤醒 将这个节点设置为head
   setHead(node);
   // propagate 一般都会大于0 或者存在可被唤醒的线程
   if (propagate > 0 || h == null || h.waitStatus < 0 ||
       (h = head) == null || h.waitStatus < 0) {
       Node s = node.next;
        // 只有一个节点 或者是共享模式 释放所有等待线程 各自尝试抢占锁
       if (s == null || s.isShared())
           doReleaseShared();
   }
}

10 Node 中的waitStatus四种状态

//线程已被 cancelled ,这种状态的节点将会被忽略,并移出队列
static final int CANCELLED =  1;
// 表示当前线程已被挂起,并且后继节点可以尝试抢占锁
static final int SIGNAL    = -1;
//线程正在等待某些条件
static final int CONDITION = -2;
//共享模式下 无条件所有等待线程尝试抢占锁
static final int PROPAGATE = -3;

11 countDown()

//将计数值减一
//递减锁重入次数,当state=0时唤醒所有阻塞线程
public void countDown() {
       sync.releaseShared(1);
}

12 releaseShared(1)

// AQS类
public final boolean releaseShared(int arg) {
    // arg 为固定值 1
    // 如果计数器state 为0 返回true,前提是调用 countDown() 之前不能已经为0
       if (tryReleaseShared(arg)) {
           // 唤醒等待队列的线程
           doReleaseShared();
           return true;
       }
       return false;
}
// CountDownLatch 重写的方法
protected boolean tryReleaseShared(int releases) {
           // Decrement count; signal when transition to zero
     // 依然是循环+CAS配合 实现计数器减1
           for (;;) {
               int c = getState();
               if (c == 0)
                   return false;
               int nextc = c-1;
               if (compareAndSetState(c, nextc))
                   return nextc == 0;
           }
}
// AQS类
private void doReleaseShared() {
       for (;;) {
           Node h = head;
           if (h != null && h != tail) {
               int ws = h.waitStatus;
               // 如果节点状态为SIGNAL,则他的next节点也可以尝试被唤醒
               if (ws == Node.SIGNAL) {
                   if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                       continue;            // loop to recheck cases
                   unparkSuccessor(h);//成功则唤醒线程
               }
               // 将节点状态设置为PROPAGATE,表示要向下传播,依次唤醒
               else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                   continue;                // loop on failed CAS
           }
           if (h == head)                   // loop if head changed
               break;
       }
}

总结 1、AQS 分为独占模式和共享模式,CountDownLatch 使用了它的共享模式。 2、AQS 当第一个等待线程(被包装为 Node)要入队的时候,要保证存在一个 head 节点,这个 head 节点不关联线程,也就是一个虚节点。 3、当队列中的等待节点(关联线程的,非 head 节点)抢到锁,将这个节点设置为 head 节点。 4、第一次自旋抢锁失败后,waitStatus 会被设置为 -1(SIGNAL),第二次再失败,就会被 LockSupport 阻塞挂起。 5、如果一个节点的前置节点为 SIGNAL 状态,则这个节点可以尝试抢占锁。

03

具体实现

01 实现逻辑 1、初始化CountDownLatch实际就是设置了AQS的state为计数的值 2、调用CountDownLatch的countDown方法时实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state值 3、调用await方法实际就调用AQS的共享式获取同步状态的方法acquireSharedInterruptibly(1),这个方法的实现逻辑就调用子类Sync的tryAcquireShared方法,只有当子类Sync的tryAcquireShared方法返回大于0的值时才算获取同步状态成功,否则就会一直在死循环中不断重试,直到tryAcquireShared方法返回大于等于0的值,而Sync的tryAcquireShared方法只有当AQS中的state值为0时才会返回1,否则都返回-1,也就相当于只有当AQS的state值为0时,await方法才会执行成功,否则就会一直处于死循环中不断重试。

02 示例

public class CountDownLatchDemo {
   public static void main(String[] args) throws InterruptedException {
       /*创建CountDownLatch实例,计数器的值初始化为5*/
       final CountDownLatch downLatch = new CountDownLatch(5);

       /*每个线程等待1s,表示执行比较耗时的任务*/
       for(int i = 0;i < 5;i++){
           final int num = i;
           new Thread(new Runnable() {
               public void run() {
                   try {
                       Thread.sleep(1000);

                   }catch (InterruptedException e){
                       e.printStackTrace();

                   }

                   System.out.println(String.format("thread %d has finished",num));

                   /*任务完成后调用CountDownLatch的countDown()方法*/
                   downLatch.countDown();
               }
           }).start();
       }

       /*主线程调用await()方法,等到其他5个线程执行完后才继续执行*/
       downLatch.await();

       System.out.println("all threads have finished,main thread will continue run");
   }
}

输出

thread 1 has finished
thread 2 has finished
thread 0 has finished
thread 3 has finished
thread 4 has finished
all threads have finished,main thread will continue run

04

使用场景

1 需要等待某个条件达到要求后才能做后面的事情 2 同时当线程都完成后也会触发事件,以便进行后面的操作

推荐阅读:

  • 深入解析HashMap和ConcurrentHashMap源码以及底层原理

  • 设计模式(二):几种工厂模式详解

  • 进程同步的五种机制以及优缺点(翻译)

  • redis五种数据类型的实现方式,常用命令,应用场景

  • redis和memcahed的共同点,区别以及应用场景

  • 详解TCP的三次握手与四次挥手及面试题(很全面)

  • Arrays 工具类详解(超详细)

  • 算法必须掌握几种方法

  • QPS、TPS、并发用户数、吞吐量

  • 设计模式之单例模式

  • Collections 工具类详解(超详细)

END

扫描二维码 | 关注我们 微信公众号 : jiagoudiantang CSDN : https://fking.blog.csdn.net