最近学了并发编程,想谈谈自己的一些理解。一来是为了加深自己的印象,二来也希望能和大家公共学习。不对的地方请斧正,谢谢!
之前说了TheadLocal,现在来讲一下CountdownLatch。说起CountdownLatch就不得不说AQS,不了解AQS的同学可以去看看我之前写得关于AQS的文章,因为CountdownLatch是基于AQS来实现的。所以,如果想更好的了解CountdownLatch,那么了解AQS的工作机制和原理是并不可少的。
这里顺便说一下CAS操作:
CAS(compareAndSwap)也就是原子操作的底层,要么都成功,那么都失败。
CAS(V,expect,update)
V表示当前内存中的值,而expect表示当前读取到的值,只有当当前读取到的值和内存中的值一致时,我们会把内存中的时更新为最新的值,即update;若不等,则说明该值被其他线程修改了;
(自旋情况)每次CAS操作时,一定有一个线程会成功更新变量,而其他失败的线程不会挂起,而是允许继续重试,直到成功为止。
这里注意一下,CAS是乐观锁:先执行了再说,执行完了再保证数据的安全
ok,接下来让我们来认识下countdownLatch
countdownLatch其实我们可以称它为计数器,用与等待某些任务执行完成再去执行下面的任务,如下图:在我们设置的数扣完之前,我们无法执行await()后面的工作。当扣减为0时,我们就可以执行红线后面的工作了,否则将在红线处等待。
下面我们就来详细说一下它对应的源码:
先来看看它其中的所有方法
好了,我们来看看这个sync。它实际上是继承了AQS的,然后重写了tryAcquireShared()和tryReleaseShared()方法。
我们先看看CountdownLatch这个方法。
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
首先,传递的count只不能小于0,实际上你如果传递了0,其实也没有任何意义了。
然后我们看到了sync的有参构造方法,其中调用了一个很熟悉的方法,setState()。之前我们在AQS中说了,state就相当于一个计数器,你设置多少,就有多少个。当state为0时,说明没有任何线程获得同步状态,大于0则说明当前是有线程获得了同步状态的。如果不了解这个,你可以把他当成一个计数器,设置为3,就说明有三个线程已经获取了同步状态。而我们在countdownlatch中所作的操作就是从3开始减,然后让它为0时进行自己的操作。
Sync(int count) { setState(count); }
接着,让我们来看看countdown()方法,它调用了sync的releaseShared()方法(释放锁,就相当于减去state)。
public void countDown() { sync.releaseShared(1); }
点击进入该方法,我们进去了AQS类中
首先我们先来看看这个tryReleaseShared方法,而这个方法,就是我们在CountdownLatch类中,sync继承了AQS时重写的方法。
protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero //自旋操作 —— 就是为了如果CAS操作不成功让他一直去执行 for (;;) { //得到当前state的值 int c = getState(); //如果为0,返回false if (c == 0) return false; //不为0,进行-1操作 int nextc = c-1; //接着,通过CAS设置最新的state的值。 if (compareAndSetState(c, nextc)) //如果设置成功,并且state当前为0,那么满足这个判断,返回true,否则返回false return nextc == 0; } } }
然后返回为true(代表state已经等于0了),这时候调用doReleaseShared()方法:
private void doReleaseShared() { //依然是自旋操作 for (;;) { //得到同步队列的头节点 —— 如果不懂请一定去看看之前的那篇AQS的文章 Node h = head; //如果头节点不为空,并且头节点不是尾结点 if (h != null && h != tail) { //获取当前头节点的同步状态 int ws = h.waitStatus; //如果为-1(我都准备好了,随时都可以获取同步状态) if (ws == Node.SIGNAL) { //那么进行CAS操作,修改当前节点的状态为0 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //不成功,那么再一次循环 continue; // loop to recheck cases //成功了,唤醒离头节点最新的节点(这个方法这里我就不讲了,不懂得去看下AQS就ok) unparkSuccessor(h); } //如果当前状态为初始化状态且当前状态利用CAS操作修改初始化状态为-3失败(PROPAGATE,只有在线程处于SHARED模式才会被使用该状态) else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //再一次循环判断 continue; // loop on failed CAS } //如果是仅仅是头节点,那么跳出循环 if (h == head) // loop if head changed break; } }
这里说明一下,返回true是代表countdown()操作成功。false则是操作失败,至于什么时候操作失败,则是在state为0时,这时候已经能再去-1了。
最后来看看await()方法,它依然调用了sync的获取中断方法
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
点进去,依然后跳到了AQS类中。这里说明一下,由于Sync继承了AQS,所以countdownLatch是直接使用了它的父类方法。而使用AQS统一方式都是这样,你只需要区分到底是独占锁还是共享锁,进而选择重写AQS给你提供的方法。即可完成你所需要的同步锁(把这个记成一个固定模式即可,AQS说白了就是一个框架,一个工具)
首先该方法中先判断了线程是否被中断,如果没有中断。那么调用tryAcquireShared()方法:
protected int tryAcquireShared(int acquires) { //判断state是否为0,如果是返回1.不是返回-1 return (getState() == 0) ? 1 : -1; }
如果上图所示,如果返回-1。表示state不为0,则进入下面的方法doAcquireSharedInterruptibly();(这里这个arg变量已经固定为1了,在我们调用方法时,countdownlatch类已经写死为1了)
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //创建一个Node节点 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { //自旋 for (;;) { //得到该节点的上一个节点 final Node p = node.predecessor(); //如果上一个节点为头节点 if (p == head) { //执行获取锁操作(之前说的重写方法) int r = tryAcquireShared(arg); //如果大于0,说明为1.这时候state已经等于0了 if (r >= 0) { //设置该节点为头节点 setHeadAndPropagate(node, r); //将上一个节点的指向为null p.next = null; // help GC failed = false; return; } } //如果不是头节点,那么进行阻塞等操作。等待被唤醒 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
现在大家应该大致可以理解,为什么await能够等待计数器变成0的时候,才会执行后面的代码了吧。从原理上来讲,await()方法就是一个获取锁的方法,但是前提是计数器为0时。而countdown()方法又是释放锁的方法,每成功一次就-1,直到state为0。每个线程进来执行完了自己的方法就调用一次countdown,当这些线程都完成了自己方法await()后面的方法才会执行。就像我们刚刚那张图一样
下面总结一下:
countdownlatch是并发的一种工具,通常用在多线程统计之类的的操作上(生成Excel之后打包zip等)。但是,一定要设置好计数器的state个数,有几个线程就设置为多少,然后每个线程执行完成之后去调用countdown()方法,代表本线程的任务已经完成了。等待所有线程的任务都完成了,我们再在设置好await()的位置去执行主线程的方法。
最后提供一个简单的demo供大家参考,一定要记住的是:设置的state数量一定要和我们另起的线程数量相同
public class UseCountDownLatch { static CountDownLatch latch = new CountDownLatch(6); /*初始化线程*/ private static class InitThread implements Runnable{ public void run() { System.out.println("Thread_"+Thread.currentThread().getId() +" ready init work......"); latch.countDown(); for(int i =0;i<2;i++) { System.out.println("Thread_"+Thread.currentThread().getId() +" ........continue do its work"); } } } /*业务线程等待latch的计数器为0完成*/ private static class BusiThread implements Runnable{ public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for(int i =0;i<3;i++) { System.out.println("BusiThread_"+Thread.currentThread().getId() +" do business-----"); } } } public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { public void run() { SleepTools.ms(1); System.out.println("Thread_"+Thread.currentThread().getId() +" ready init work step 1st......"); latch.countDown(); System.out.println("begin step 2nd......."); SleepTools.ms(1); System.out.println("Thread_"+Thread.currentThread().getId() +" ready init work step 2nd......"); latch.countDown(); } }).start(); new Thread(new BusiThread()).start(); for(int i=0;i<=3;i++){ Thread thread = new Thread(new InitThread()); thread.start(); } latch.await(); System.out.println("Main do ites work........"); } }