引言及简介
前面我们介绍了独占锁ReentrantLock实现的一个同步辅助工具CyclicBarrier, 它能够使一组线程互相等待,今天我们介绍另一种同步辅助器CountDownLatch,它其实可以看着是利用共享锁实现的,只不过它没有使用到类似共享锁Semaphore那么复杂的逻辑,所以它的实现没有直接利用Semaphore完成,而是直接在AQS的共享式获取/释放同步资源的基础上实现的一个非常简单的同步辅助工具。
根据Java Doc的描述,CountDownLatch可以在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。类比CyclicBarrier的一组线程之间的相互等待,CountDownLatch则是一个或一组线程等待另一个或另一组线程,所以CountDownLatch与CyclicBarrier各自等待的线程是不同的。并且CountDownLatch内部维护的计数器是不能重置循环使用的,而CyclicBarrier的计数器是可以被重置循环使用的。
通俗的讲,在用给定的计数初始化CountDownLatch之后,所有执行了await()方法的线程必须要等待其他线程执行相应次数的countDown()之后才会返回,否则将一直阻塞。相当于CountDownLatch维护了一个计数器,执行了await()方法的线程必须要等到计数器清零之后才会返回,而其他线程每执行一次countDown(),计数器就会减1,所以其实执行await()方法的线程可以是多个,而执行countDown()的线程也可以是一个线程的多次执行,因为countDown()方法的执行不存在阻塞等待的情况。
使用示例
假设某个主任务在执行过程中,分别需要满足2个条件才能继续往下执行,这两个条件分别由另外两个线程去满足:
final CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); Thread.sleep(3000); System.out.println("主线程"+Thread.currentThread().getName()+"达成主线程的条件1"); latch.countDown(); System.out.println("子线程"+Thread.currentThread().getName()+"继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); Thread.sleep(5000); System.out.println("子线程"+Thread.currentThread().getName()+"达成主线程的条件2"); latch.countDown(); System.out.println("子线程"+Thread.currentThread().getName()+"继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); try { System.out.println("等待2个子线程达成主线程需要的2个条件..."); latch.await(); System.out.println("2个子线程都达成了主线程需要的条件,主线程继续执行"); } catch (InterruptedException e) { e.printStackTrace(); }
执行结果:
子线程Thread-0正在执行 等待2个子线程达成主线程需要的2个条件... 子线程Thread-1正在执行 主线程Thread-0达成主线程的条件1 子线程Thread-0继续执行 子线程Thread-1达成主线程的条件2 2个子线程都达成了主线程需要的条件,主线程继续执行 子线程Thread-1继续执行
由上面的示例可以看到,CountDownLatch初始化为2,所以需要执行两次countDown()才会返回,否则就一直等到,示例中就是主线程等待2两个子线程,而两个子线程之间不存在相互影响或等待,子线程Thread-0执行了3秒钟就达成了条件1,然后继续做自己的事情,子线程Thread-1却花了5秒钟才达成条件2,然后主线程立即继续往下执行。两个子线程之间不存在相互等待或影响。
源码分析
首先,看看CountDownLatch的类结构
从上图可以看出,CountDownLatch非常简单,没有实现和继承任何接口和父类,直接将操作都代理到继承了AQS的静态内部类Sync上。CountDownLatch对外提供的最主要的方法就await/await(timeout)以及countDown():
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //直到超时,被中断,或者成功获取到同步资源才返回 //如果在超时到达之前成功获取资源返回true,否则返回false //该返回值的含义其实也表示是否在超时到达之前,其他线程全部都达到指定的位置(即都执行了countDown()方法将计数器减到0) public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } //释放一个同步资源 public void countDown() { sync.releaseShared(1); } public long getCount() { return sync.getCount(); }
从源码可以发现,CountDownLatch确实利用的AQS的共享锁的逻辑,await必须要获取到一个同步资源才能返回,而countDown则是释放一个同步资源,接着看Sync的逻辑:
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { //初始化方法,即初始化CountDownLatch时传入的计数器 setState(count); } int getCount() { //获取当前剩余的同步资源个数 return getState(); } //await方法最终的调用次方法尝试获取同步资源 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; //只有当剩余同步资源为0时才表示成功。 } //countDown方法最终的调用方法-释放同步资源 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) //已经清0,直接返回false return false; int nextc = c-1;//直接做减1操作,如果成功,并且减到0才返回true,这样才会唤醒调用await阻塞的线程 if (compareAndSetState(c, nextc)) return nextc == 0; } } }
由以上的源码可以很清楚CountDownLatch的实现,其原理大致是,使用CountDownLatch的构造方法传入一个正整形数字的参数之后,由AQS维护这相同数量的同步资源个数,调用await的线程除非在线程被中断,等待超时(如果有超时时间的话),或者该同步资源个数为0的时候才会返回,否则一直等待。而当有线程执行了countDown方法之后,如果当前同步资源个数不为0就减1,每执行一次countDown就减一次1,直到减到0才会唤醒等待的线程。减到0之后,如果继续执行countDown则不会有任何反应。
所以,当await()方法因为其他线程执行了countDown将计数器减至0被唤醒之后,再次调用await()方法时,肯定是会立即返回的,不论有没有执行countDown,因为其计数器一旦被清0,将无法被重新还原,这也就是CountDownLatch不能被重用的原因。
内存可见性
由于CountDownLatch其内部机制其实就是对声明在AQS中的volatile修饰的state变量的维护,所以CountDownLatch也自然满足volatile语义带来的happens-before原则,即“对一个volatile变量的写操作先行发生于后面对这个变量的读操作”, 因此可以得出,在其他线程中调用countDown(写volatile变量)之前的操作happens-before另一个线程执行await(读volatile变量)返回的操作。
也就是说,在其他线程执行countDown之前对共享变量的修改对执行await的线程在await方法返回之后是立即可见的。而那些执行countDown的多个线程(如果存在多个线程的话)之间却不能得出可见性结论。
相关推荐
8. **CyclicBarrier和CountDownLatch**:这两个是同步辅助类。CyclicBarrier允许多个线程等待直到所有线程到达屏障点后一起继续;CountDownLatch则是一次性的计数器,线程调用countDown()方法,当计数到零时所有等待...
Java并发包还提供了一系列的同步辅助类,比如CountDownLatch、CyclicBarrier、Semaphore等。这些类可以用来控制线程的执行流程,例如CountDownLatch用于让一个或多个线程等待直到在其他线程中的一组操作完成。 ### ...
4. **CountDownLatch**:这是一个一次性使用的同步辅助类,用于让一组线程等待其他线程完成操作。在批量处理中,主线程可能使用CountDownLatch来等待所有子线程完成任务,然后继续执行后续操作。 5. **...
在并发程序设计中,我们需要控制多个线程的执行流程,CountDownLatch是一种同步辅助类,允许一个或多个线程等待其他线程完成操作。CyclicBarrier是一种用于多线程协作的同步工具,允许一组线程相互等待直到所有线程...
而CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。使用这两个工具类可以构建出复杂多变的同步场景。 总结来说,锁是并发编程中解决资源竞争问题的重要...