CountDownLatch是Java并发编程中非常常用的一种同步工具。它可以让一个或多个线程等待其他线程完成操作后再继续执行。通过CountDownLatch,我们可以更好地控制程序的执行顺序和并发度。
在本文中,我们将介绍CountDownLatch的概念、用法和示例,并深入讲解它在多线程编程中的应用场景和优势。同时,我们也将探讨CountDownLatch的一些实现细节和注意事项,帮助读者更好地理解和使用这个工具。
无论你是Java初学者还是有经验的开发者,本文都能够帮助你更好地掌握CountDownLatch的使用方法和技巧。希望通过本文的介绍,读者能够更加深入地理解Java并发编程,提升自己的编程能力。
一、CountDownLatch是什么?
Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。
你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。
下面是一个非常典型的CountDownLatch的应用场景是协调多个线程的执行。
例如,假设我们有三个线程A、B、C,它们分别完成不同的任务,但是A需要等待B和C都执行完之后才能开始自己的任务。这时候,我们就可以使用一个CountDownLatch来实现线程之间的协调。
图片来源于网络,侵删
具体的实现步骤如下:
创建一个CountDownLatch对象,初始计数器的值为2,表示需要等待两个线程执行完毕。
在B和C线程的任务执行完毕后,调用CountDownLatch的countDown()方法,将计数器的值减1。
在A线程中,调用CountDownLatch的await()方法,等待计数器的值变为0,表示B和C线程都已经执行完毕。
二、怎么使用CountDownLatch
构造方法
CountDownLatch(int count)构造器就是用来创建一个CountDownLatch对象的,其中count参数表示需要等待的线程数。
//参数count为计数值
public CountDownLatch(int count) {};
重要方法
await() 方法用于使当前线程等待 CountDownLatch 计数器减至 0 的信号。调用 await() 方法的线程会被阻塞,直到计数器的值为 0。一般情况下,需要在另一个线程中调用 countDown() 方法来减少计数器的值。当计数器的值减至 0 时,所有等待的线程都会被唤醒并可以继续执行。
await(long timeout, TimeUnit unit)与 await() 方法类似,但它可以设置一个超时时间,表示当前线程最多等待的时间。如果在指定时间内计数器的值没有减至 0,则等待的线程将不再被阻塞,可以继续执行后续操作。
countDown()用于将计数器的值减 1。通常在其他线程中调用此方法,用于控制多个线程的执行顺序。每次调用 countDown() 方法都会使计数器的值减 1,直到计数器的值减至 0 时,等待的线程才会被唤醒。
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
三、使用案例
案例解析
这个案例主要演示了Java中如何使用CountDownLatch类实现多个线程间的同步。
具体来说,代码中创建了一个CountDownLatch对象latch,并将计数器初始化为2。然后创建了两个线程,在每个线程中执行一些任务,最后在任务执行完毕后通过调用latch.countDown()来减少计数器的值。当计数器的值减到0时,主线程中的latch.await()方法将被唤醒,从而实现了线程间的同步。
通过这个案例,我们可以更好地理解CountDownLatch的使用方式和原理。
1、首先是创建实例;
CountDownLatch countDown = new CountDownLatch(2)
2、需要同步线程执行后,计数-1;countDown.countDown();
3、需要等待其他线程执行完毕后,再运行的线程,调研countDown.await()实现阻同步;
示例代码:
package com.util;
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
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()
+ "执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread() {
public void run() {
try {
System.out.println("子线程" + Thread.currentThread().getName()
+ "正在执行");
Thread.sleep(3000);
System.out.println("子线程" + Thread.currentThread().getName()
+ "执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果:
子线程Thread-0正在执行
等待2个子线程执行完毕...
子线程Thread-1正在执行
子线程Thread-0执行完毕
子线程Thread-1执行完毕
2个子线程已经执行完毕
继续执行主线程
四、CountDownLatch的使用场景
前面给了一个demo演示如何使用,因为他在现实场景确实有很广泛的应用,比如电商的详情页,由众多的数据拼装组成,如可以分成一下几个模块:
前面给了一个demo演示如何使用,因为他在现实场景确实有很广泛的应用,比如电商的详情页,由众多的数据拼装组成,如可以分成一下几个模块:
- 交易的收发货地址,销量;
- 商品的基本信息(标题,图文详情之类的);
- 推荐的商品列表;
- 评价的内容;
上面的几个模块信息,都是从不同的服务获取信息,且彼此没啥关联;所以为了提高响应,完全可以做成并发获取数据,如:
- 线程1获取交易相关数据;
- 线程2获取商品基本信息;
- 线程3获取推荐的信息;
- 线程4获取评价信息;
但是最终拼装数据并返回给前端,需要等到上面的所有信息都获取完毕之后,才能返回,这个场景就非常的适合CountDownLatch来做了。
- 拼装完整数据的线程中调用countDownLatch.await(long,TimeUnit) 等待所有的模块信息返回。
- 每个模块信息的获取,由一个独立的线程执行;执行完毕之后调用 countDownLatch.countDown()进行计数-1。
五、CountDownLatch原理
CountDownLatch在多线程并发编程中充当一个计时器的功能,
CountDownLatch在多线程并发编程中充当一个计时器的功能,并且内部维护一个count的变量,并且其操作都是原子操作,该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。
如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。比如下面的例子就是有两个操作,一个是读操作一个是写操作,现在规定必须进行完写操作才能进行读操作。所以当最开始调用读操作时,需要用await()方法使其阻塞,当写操作结束时,则需要使count等于0。因此count的初始值可以定为写操作的记录数,这样便可以使得进行完写操作,然后进行读操作。
构造方法:CountDownLatch
内部也是个Sync类继承了AQS,所以CountDownLatch类的构造方法就是调用Sync类的构造方法,然后调用setState()方法设置AQS中state的值。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
方法:await()
该方法史使调用的线程阻塞住,知道state的值为0就放开所以阻塞的线程。实现会调用到AQS中的acquireShareInterruptibly()方法,先判断是否被中断,接着调用了tryAcquireShared()方法,可以看到实现逻辑就是判断state值是否为0,是就返回1,不是就返回-1。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
方法countDown()
这个方法会对state值减1,会调用到AQS中releaseShared()方法,目的是为了调用doReleaseShared()方法,这个是AQS定义好的释放资源的方法,而tryReleaseShred()则是子类实现的,可以看到是一个自旋CAS操作,每次都获取state值,如果为0则直接返回,否则就执行减1的操作,失败了就重试,如果减完后值为0就表示要释放所有阻塞的线程了,也就会执行到AQS中
的doReleaseShared()方法。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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是Java中一个非常有用的多线程控制工具,它可以用于等待多个线程完成后再执行某些任务。
CountDownLatch的主要特点如下:
- CountDownLatch是一个线程同步工具,它允许一个或多个线程等待其他线程完成操作。
- CountDownLatch的初始化需要指定一个计数器,表示等待的线程数量。
- CountDownLatch的主要方法是countDown()和await()。每次调用countDown()方法,计数器会减1,当计数器减为0时,等待中的线程将被唤醒。而await()方法则会阻塞当前线程,直到计数器为0。
- CountDownLatch一旦初始化后,计数器不能被重置,因此它只能被使用一次。
- CountDownLatch可以用于控制多个线程同时开始执行或者等待多个线程完成后执行某些操作。
- CountDownLatch的一个常见用途是等待多个线程完成某项任务后,再进行下一步操作。
总的来说,CountDownLatch是一个非常实用的工具,可以帮助我们实现多线程控制。通过合理地使用CountDownLatch,我们可以更加方便地管理线程的执行顺序和并发性,提高代码的可读性和可维护性。