贸易公司网站建设要多少钱,wordpress这么安装不了,内江seo,网站建设公司广东1 缘起
有一次听到同事谈及AQS时#xff0c;我有很多点懵#xff0c; 只知道入队和出队#xff0c;CLH#xff08;Craig#xff0c;Landin and Hagersten#xff09;锁#xff0c;并不了解AQS的应用#xff0c; 同时结合之前遇到的多线程等待应用场景#xff0c;发现…1 缘起
有一次听到同事谈及AQS时我有很多点懵 只知道入队和出队CLHCraigLandin and Hagersten锁并不了解AQS的应用 同时结合之前遇到的多线程等待应用场景发现CountDownLatch是使用AQS实现的线程等待 本文即通过实战和源码分析探究CountDownLatch是如何利用AQS实现线程等待的 帮助读者轻松应对知识交流与考核。
2 CountDownLatch
同步辅助工具允许一个或多个线程等待其他线程操作完成。 CountDownLatch通过给定同步状态数量初始化即state数量参见AQSAbstractQueuedSynchronizer。 执行过程中会通过调用countDown方法来阻塞await方法直到同步状态state为0时释放所有阻塞的线程立即返回结果。 CountDownLatch的同步状态数量是一次性的操作无法重置次数。 如果需要多次使用计数可以使用CyclicBarrier。 CountDownLatch是通用的同步工具用途广泛。 CountDownLatch初始化同步状态数量为1可以作为一个简单的开/关锁存器或门所有调用await方法的线程都在门处等待直到线程调用countDown方法开启门。 CountDownLatch初始化同步状态数量为N线程等待N个线程完成操作或某个线程完成N次。 CountDownLatch非常有用的一个特性是执行前不要求调用countDown的线程等待同步状态数量达到0仅仅是通过await方法阻止线程通过直到所有任务都完成一次性通过所有线程。 内存一致性影响同步状态数量达到0之前先调用countDown()的线程执行优先于其他线程调用await获取结果。
2.1 测试样例
CountDownLatch两个核心方法countDown和await 其中 countDown同步状态数量减1CountDownLatch继承AbstractQueuedSynchronizerAQS通过state属性检测是否获取或释放当前资源所以countDown方法主要的任务就是减少state值释放资源。 await则是阻塞线程state未达到0时持续自旋等待最终所有线程任务执行完成后立即返回继续执行后面的逻辑。 下面给出具体的测试样例新建CountDownLatch初始化同步状态数state3执行三次countDown释放线程继续执行。
package com.monkey.java_study.juc;import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Random;
import java.util.concurrent.CountDownLatch;/*** CountDownLatch测试.** author xindaqi* since 2023-02-20 15:00*/
public class CountDownLatchTest {private static final Logger logger LoggerFactory.getLogger(CountDownLatchTest.class);/*** 线程执行器* 执行countDown计数器减一*/static class ThreadRunner implements Runnable {private final CountDownLatch countDownLatch;public ThreadRunner(CountDownLatch countDownLatch) {this.countDownLatch countDownLatch;}Overridepublic void run() {try {StopWatch stopWatch new StopWatch();stopWatch.start();Random random new Random();int randomBound 1000;Thread.sleep(random.nextInt(randomBound));stopWatch.stop();logger.info({}, time cost:{}, Thread.currentThread().getName(), stopWatch.formatTime());countDownLatch.countDown();} catch (Exception ex) {throw new RuntimeException(ex);}}}public static void main(String[] args) {StopWatch stopWatch new StopWatch();stopWatch.start();int threadCount 3;CountDownLatch countDownLatch new CountDownLatch(threadCount);for (int i 0; i 3; i) {new Thread(new ThreadRunner(countDownLatch)).start();}try {countDownLatch.await();stopWatch.stop();logger.info(Time cost:{}, stopWatch.formatTime());} catch (Exception ex) {throw new RuntimeException(ex);}}
}测试结果如下图所示 由结果可知三个线程各自执行通过不同的延迟时间模拟程序执行的时间 执行耗时最长的Thread-2执行结束后释放线程继续执行后续的逻辑。
3 源码分析
下面根据源码分析CountDownLatch是如何实现线程等待以及释放资源的。
3.1 初始化
首先从初始化CountDownLatch开始讲解源码如下图所示 由源码可知通过count参数初始化AQS的同步状态数state Sync即CountDownLatch中继承AbstractQueuedSynchronizer的内部类 通过setState(count)初始化state这个放在Sync中讲先提示一下。 位置java.util.concurrent.CountDownLatch#CountDownLatch
3.2 Sync
下面进入Sync源码如下图所示 由图可知Sync是CountDownLatch的内部私有静态类继承AbstractQueuedSynchronizer类 构造函数参数为count用于初始化state即setState(count) 通过getCount()调用getState方法获取同步状态数量。 同时重写了tryAcquireShared和tryReleaseShared两个方法 用于获取共享资源和释放共享资源。 位置java.util.concurrent.CountDownLatch.Sync 获取共享资源和释放共享资源的源码如下图所示 由图可知tryAcquireShared通过获取同步状态数量标识当前资源状态 同步状态不为0时返回-1为0时返回1为后续提供操作标识后文会反复用到到时详解。
3.3 countDown
同步状态减1通过countDown方法实现源码如下图所示 由图可知该方法调用了releaseShared方法实现状态减1这里还看不出具体的逻辑 接着往下看进入方法releaseShared。 位置java.util.concurrent.CountDownLatch#countDown
3.3.1 releaseShared
releaseShared源码如下图所示由图可知 判断条件中调用了tryReleaseShared方法为true时才会调用doReleaseShared否则直接返回。 其中tryReleasShared方法由前文可知 Sync内部类继承AQS重写了该方法实现同步状态state减1state0返回fasle否则返回true 由此可知releaseShared方法通过tryReleasShared实现状态减1在state不为0时不会调用doReleaseShared方法。 当state0时才会调用doReleaseShared方法此时所有线程已经完成任务将要全部通过latch门闩 由上述可推测doReleaseShared用于释放资源。 这个方法并没有实现自旋等待而是在await方法中进行等待的在state没有达到0时不会让线程通过。 位置java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared tryReleaseShared源码标注如下图所示 由图可知通过自旋的方式使同步状态减1state0返回true否则返回false供后续方法判断。 位置java.util.concurrent.CountDownLatch.Sync#tryReleaseShared
doReleaseShared则是释放资源源码如下图所示 由图可知通过修改节点等待状态waitStatus为0以及取消标记unparkSuccessor释放线程这个放在后面讲 通过自旋的方式依次释放节点标记的线程资源最终释放所有标记的资源。 位置java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared unparkSuccessor释放节点标记的线程源码如下图所示 由源码可知获取当前节点的后继节点 后继节点不为空时通过LockSupport.unpark释放线程标记 当节点的后继节点不为空时会释放当前节点标记的线程即节点不再等待线程直接释放 实现最后一个线程执行结束后取消节点对线程的占用释放资源。 这里直接取消线程标记也是导致同步状态state无法重用的直接原因已经取消标记后续自然无法使用 同时state置为0后没有重新配置所以依旧不能重用state。
3.4 await
综上分析CountDownLatch控制线程等待的逻辑是在await中实现的 因为上面的逻辑只是实现了同步状态减1以及释放线程标记等工作没有阻塞线程 所以在await阻塞线程当所有线程均完成各自逻辑时即同步状态state0才开启“闸门”释放锁。 await源码如下图所示由图可知await调用acquireSharedInterruptly方法 当同步状态state0时立即返回。 位置java.util.concurrent.CountDownLatch#await()
3.4.1 acquireSharedInterruptibly
线程等待。同步状态state不为0时线程间相互等待。 acquireSharedInterruptibly源码如下图所示 由图可知判断条件通过tryAcquireShared获取当state不为0时返回-1参见上文的tryAcquireShared方法 调用doAcquireSharedInterrutly方法实现线程等待。 位置java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
3.4.2 doAcquireSharedInterruptily
线程等待具体实现源码如下图所示 由图可知每次调用都会向队列插入一个新的节点Node.SHARED 然后自旋即线程等待当前节点的前驱节点为头节点时获取同步状态state state不为0时tryAcquireShared-1不会跳出自旋 继续等待countDown操作 当countDown使state减为0时tryAcquireShared返回1开始执行r0后续的逻辑 跳出自旋立即返回即await立即返回继续执行await后续的逻辑释放线程等待。
位置java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 上面提到获取当前节点的前驱节点通过predecessor方法 源码如下图所示由图可知返回节点的前驱节点。 位置java.util.concurrent.locks.AbstractQueuedSynchronizer.Node#predecessor
至此完成CountDownLatch多线程同步状态递减以及相互等待的分析。
4 小结
1CountDownLatch是一次性的线程等待工具实现多线程相互等待直到所有线程均完成执行逻辑用完即抛无法重用 2countDown方法使同步状态减1当一个线程执行完成后调用countDown标识当前线程已完成当state0时释放节点的线程占用这也是CountDownLatch无法重用的直接原因当然没有重复保存state也是另一个原因 3await是线程等待的核心当同步状态state不为0时会自旋等待而不触发跳出自旋当state0时才会立即返回跳出自旋结束线程等待 4当某个线程出现异常时这里需要格外注意为保证程序正常中断需要手动捕获异常同时调用countDown方法或者使用重试机制否则线程不会退出因为state最终的状态不为0有一个线程线程而没有减1。