Java并发工具类CountDownLatch原理剖析

Java (47) 2024-02-08 09:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说Java并发工具类CountDownLatch原理剖析,希望能够帮助你!!!。

本篇文章的主要内容如下:

1:举例说明CountDownLatch和Thread的join()方法
2:CountDownLatch的原理剖析
3:CountDownLatch的源码解析

1:举例说明CountDownLatch和Thread的join()方法

在实际的项目中,我们可能碰到如下的需求:

A线程需要用到B线程,C线程,D线程的结果

像上面的需求我们怎样利用程序去实现的。

实现一:熟悉Thread类的同学很快就能够想到join()方法,程序如下:

public class ThreadJoinTest {
 private static int totalCount;
 private static int count1;
 private static int count2;
 private static int count3;
 public static void main(String[] args) throws InterruptedException {
 Thread t1 = new Thread(() -> count1 = 10);
 Thread t2 = new Thread(() -> count2 = 20);
 Thread t3 = new Thread(() -> count3 = 30);
 t1.start();
 t2.start();
 t3.start();
 t1.join();
 t2.join();
 t3.join();
 totalCount = count1 + count2 + count3;
 System.out.println("totalCount=" + totalCount);
 }
}

运行结果如下:

Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第1张

上面的程序无论运行多少次都是同样的结果,我们看一下Thread的join()方法的原理:如果线程thread调用join()方法,那么它会阻塞当前正在运行的线程,记着是阻塞当前正在运行的线程,而不是阻塞thread线程,直到thread线程运行结束,才会继续往下执行

例如上面的例子中主线程执行到t1.join()方法时,主线程会被阻塞,直到线程t1运行结束,才能继续往下执行t2.join(),当执行t2.join()方法时,主线程序又被阻塞直到线程t2运行结束,以此类推,我们想象以下如果不调用join()方法,那结果是无法预知的。

实现二:利用CountDownLatch实现
public class CountDownLatchTest {
 private static int totalCount;
 private static int count1;
 private static int count2;
 private static int count3;
 private static CountDownLatch cdl = new CountDownLatch(3);
 public static void main(String[] args) throws InterruptedException {
 new Thread(()->{
 count1=10;
 cdl.countDown();
 }).start();
 new Thread(()->{
 count2=20;
 cdl.countDown();
 }).start();
 new Thread(()->{
 count3=30;
 cdl.countDown();
 }).start();
 cdl.await();
 totalCount = count1+count2+count3;
 System.out.println("totalCount=" + totalCount);
 }
}

运行结果如下:

Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第2张

从上面的运行结果可以看出,CountDownLatch也能实现join的效果,那它的实现原理是什么呢?和join()方法又有哪些区别呢?接下来从源码角度去揭开它的原理。

2:CountDownLatch的原理剖析

CountDownLatch是一个并发工具类,它允许一个线程或者多个线程等待,直到一系列操作在其他线程被完成,这句话蕴含了两层含义:

第一层含义:允许一个线程或者多个线程等待,是说明调用它的await()方法会使当前运行的线程阻塞。
第二层含义:直到一系列操作在其他线程被完成,这个完成点就是调用了countDown()方法

CountDownLatch构造函数接收一个计数器,调用一次countDown()方法,计数器就会减1,只有计数器变为0时,等待的线程才能从await()方法返回继续执行下面的代码,例如如果初始化的计数器是2,那么必须调用2次countDown()方法,被阻塞的线程才能从await()中返回,继续执行下面的代码。

在我们启动一个服务时,可能有比较耗时的工作需要其他线程协助同时完成,只有这些工作完成后,主服务线程才能执行下去,我们可以利用Thread的join()方法完成,也可以通过CountDownLatch完成,但是两者又有所区别。

1:对于join(),必须线程逻辑完全执行,等待线程才能从阻塞中返回。
2:对与CountDownLatch则非常的灵活了,它能保证调用countDown()方法前的逻辑能够在等待线程前执行,countDown()方法后的代码逻辑那就不一定了。

CountDownLatch的底层实现逻辑就是AQS共享资源模式的逻辑,其中初始化的计数器就是AQS共享资源state。

await()方法会阻塞当前线程,直到出现下面的情况才返回

1:计数器变成了0
2:被其他线程中断

countDown()方法的作用就是将计数器减一

对于这个并发工具类总结四个重要的知识点如下:

1:CountDownLatch底层是通过AQS框架的共享模式操作完成的。
2:调用构造函数传递计数器count,并且计数器不能小于0,计数器会赋值给底层AQS的state。
3:调用await()会阻塞当前线程,直到线程被中断或者count==0才返回
4:每调用一次countDown()方法会将计时器count减一

3:CountDownLatch的源码解析

上面我们知道了它的原理,接下来我们从源码角度看一下它的实现。

在CountDownLatch源码中有这样一段代码:

private static final class Sync extends AbstractQueuedSynchronizer {
 //设置共享资源state
 Sync(int count) {
 setState(count);
 }
 int getCount() {
 return getState();
 }
//尝试获取共享资源
 protected int tryAcquireShared(int acquires) {
 return (getState() == 0) ? 1 : -1;
 }
//尝试释放共享资源
 protected boolean tryReleaseShared(int releases) {
 // Decrement count; signal when transition to zero
 for (;;) {
 int c = getState();
 if (c == 0)
 return false;
 int nextc = c-1;
 if (compareAndSetState(c, nextc))
 return nextc == 0;
 }
 }
}

在CountDownLatch中创建了一个内部类Sync,它继承了AQS,这也是AQS的经典用法,并且实现了两个重要的方法,尝试获取共享资源tryAcquireShared()和释放共享资源tryReleaseShared().说明底层使用的是AQS框架的共享模式。对于AQS共享模式下资源的获取和释放不熟悉的请看这一篇文章。

public CountDownLatch(int count) {
 if (count < 0) throw new IllegalArgumentException("count < 0");
 this.sync = new Sync(count);
}

上面是它的构造函数,传递一个参数计时器count,并且count必须大于等于0,如果小于0会抛出异常,然后它直接调用了Sync的构造函数,我们从Sync的构造函数可知,其实传递的count就是赋值给了AQS的共享资源变量state。

Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第3张

public void await() throws InterruptedException {
 sync.acquireSharedInterruptibly(1);
}
Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第4张

上面的代码是CountDownLatch中的await()方法的源码,它底层直接调用了Sync的acquireSharedInterruptibly()方法,这个方法有两种情况返回:

第一种:被其他线程中断
第二种:tryAcquireShared()<0

那tryAcquireShard()什么时候会小于0呢?这在Sync中可以找到答案。

Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第5张

上面我们知道了要想从await()方法返回,要么线程被中断,要么tryAcquireShared()<0,而此方法只有count==0时才小于0,上面我们在创建CountDownLatch对象时传递的共享资源是count,所以我们做的就是将count变成0,那就是我们调用countDown()方法了。

public void countDown() {
 sync.releaseShared(1);
}

上面的countDown()方法直接调用了Sync的releaseShared()方法

Java并发工具类CountDownLatch原理剖析_https://bianchenghao6.com/blog_Java_第6张

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

发表回复