Java并发之CountDownLatch
目录
4、实例:一个线程等待其他线程运算结果,其他线程不需要阻塞等待
7.3 countDown 与CyclicBarrier的区别:
8、AbstractQueuedSynchronizer (简称AQS)
1、什么是CountDownLatch
同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。
比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
闭锁是一种同步工具类,可以延迟线程的进度,直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能够通过,当达到结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。
闭锁可以确保某些活动直到其他活动都完成之后才继续执行。
CountDownLatch是一种灵活的闭锁实现,它允许一个或多个线程等待一组事件的产生。
闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。
2、CountDownLatch如何工作的
正如Java文档所描述的那样,CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
CountDownLatch是在Java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,
然后在闭锁上等待的线程就可以恢复执行任务。
3、方法说明
// 构造器,必须指定一个大于零的计数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。
// 这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。
// 计数-1
public void countDown() {
sync.releaseShared(1);
}
// 获取计数
public long getCount() {
return sync.getCount();
}
// 其他线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。
// 这种通知机制是通过CountDownLatch.countDown()方法来完成的,每调用一次这个方法,在构造函数中初始化的count值就减1.所以当N个线程都调用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
// 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 线程阻塞一段时间,如果计数依然不是0,则返回false;否则返回true
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
必须有线程中显示的调用了countDown()计数-1方法;必须有线程显示调用了 await()方法(没有这个就没有必要使用CountDownLatch了)
由于await()方法会阻塞到计数为0,如果在代码逻辑中某个线程漏掉了计数-1,导致最终计数一直大于0,直接导致死锁了
鉴于上面一点,更多的推荐 await(long, TimeUnit)来替代直接使用await()方法,至少不会造成阻塞死只能重启的情况。
当线程调用了 await()实现阻塞同步,则这个线程就等待这个计数器变为0,当这个计数器变为0时,这个线程继续自己下面的工作。
4、实例:一个线程等待其他线程运算结果,其他线程不需要阻塞等待
1、多线程实现累加,多个线程不需要同时开始,一个线程等待其他线程的运算结果。此时的其他线程可以去做其他事情了,不需要阻塞等待在这了。注意和CyclicBarrier区分,参考对比更容易理解。
/**
* @author powerful
* @dec 模拟多线程运算,异步分段计算,线程1实现 1加到1000,线程2实现 1000加到2000,线程3实现 2000加到5000,线程4实现
* 线程1、2、3计算结果的和。 这个和上个例子不同,不需要统一的开始时间,只要线程1和2完成运算,线程3才能运算。
*
* 线程3 : 线程3开始运算
线程2 : 线程2开始运算
线程1 : 线程1开始运算
线程4 : 开始等待结果
线程3 : 线程3运算结束: 10498500
线程2 : 线程2运算结束: 1499500
线程1 : 线程1运算结束: 499500
线程4 : 开始计算总和
线程4 : 线程4运算结束: 12497500
*
*/
public class CountDownLatchMultithreadOperation {
private CountDownLatch countDownLatch;
private int Num1 = 1, Num2 = 1000, Num3 = 2000, Num4 = 5000;
private volatile int tmpRes1, tmpRes2, tmpRes3;
// 计算开始数值到结束数值的和
private int add(int startNum, int endNum) {
int sum = 0;
for (int i = startNum; i < endNum; i++) {
sum += i;
}
return sum;
}
// 统计所有的和的值
private int totalSum(int a, int b, int c) {
return a + b + c;
}
public void calculate() {
countDownLatch = new CountDownLatch(3);// 有3个需要处理计算的线程,第4个线程等待前3个线程运算结果
Thread thread1 = new Thread(() -> {
try {
// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
System.out.println(Thread.currentThread().getName() + " : 线程1开始运算");
Thread.sleep((long) (Math.random() * 10000));
tmpRes1 = add(Num1, Num2);
System.out.println(Thread.currentThread().getName() + " : 线程1运算结束: " + tmpRes1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}, "线程1");
Thread thread2 = new Thread(() -> {
try {
// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
System.out.println(Thread.currentThread().getName() + " : 线程2开始运算");
Thread.sleep(1000);
tmpRes2 = add(Num2, Num3);
System.out.println(Thread.currentThread().getName() + " : 线程2运算结束: " + tmpRes2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}, "线程2");
Thread thread3 = new Thread(() -> {
try {
// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
System.out.println(Thread.currentThread().getName() + " : 线程3开始运算");
Thread.sleep(100);
tmpRes3 = add(Num3, Num4);
System.out.println(Thread.currentThread().getName() + " : 线程3运算结束: " + tmpRes3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}, "线程3");
Thread thread4 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " : 开始等待结果");
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " : 开始计算总和");
int ans = totalSum(tmpRes1, tmpRes2, tmpRes3);
System.out.println(Thread.currentThread().getName() + " : 线程4运算结束: " + ans);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程4");
thread3.start();
thread4.start();
thread1.start();
thread2.start();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatchMultithreadOperation demo = new CountDownLatchMultithreadOperation();
demo.calculate();
Thread.sleep(10000);
}
}
2、多个线程同时开始,等所有线程都结束,主线程结束。
/**
* @author powerful
* @dec 模拟了百米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。
*/
public class PlayersTest {
public static void main(String[] args) throws InterruptedException {
// 开始的倒数锁,开始的信号,没有开始都在等待,当开始信号发出(执行countDown方法),
final CountDownLatch begin = new CountDownLatch(1);// 控制统一开始,信号发出即开始。
// 结束的倒数锁,所有的人跑完,所有的线程结束,执行完成。
final CountDownLatch end = new CountDownLatch(10);// 控制统一结束,所有人跑完结束。
// 模拟出10个线程代表10个参赛选手
for (int index = 0; index < 10; index++) {
final int num = index + 1;
new Thread(new Runnable() {
@Override
public void run() {
try {
// 如果当前计数为零,则此方法立即返回。
begin.await();// 等待开始命令
Thread.sleep((long) (Math.random() * 10000));
System.out.println("No." + num + " arrived");
} catch (InterruptedException ignored) {
} finally {
end.countDown(); // 选手到达终点时,end就减一
}
}
}).start();
}
System.out.println("Game Start");
begin.countDown(); // begin减一,开始游戏
System.out.println("players is running!");
end.await(); // 等待end变为0,即所有选手到达终点
System.out.println("Game Over");
}
}
6、源码分析:
// 核心代码:
CountDownLatch latch = new CountDownLatch(1);
latch.await();
latch.countDown();
其中构造函数的参数是计数器的值;
await()方法是用来阻塞线程,直到计数器的值为0 ;
countDown()方法是执行计数器-1操作。
1、构造函数的代码
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
首先if判断传入的count是否<0,如果小于0直接抛异常。
然后new一个类Sync。
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
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;
}
}
}
可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。
其中有几个核心点:
变量 state是父类AQS里面的变量,在这里的语义是计数器的值;
getState()方法也是父类AQS里的方法,很简单,就是获取state的值;
tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写。
2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判断线程是否是中断状态
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取state的值
if (tryAcquireShared(arg) < 0)//step1
doAcquireSharedInterruptibly(arg);//step2,获取共享锁失败
}
tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,
如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。
若state!=0,则返回-1,进入if体内step2处。
下面我们来看doAcquireSharedInterruptibly方法:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//step1、把当前线程封装为共享类型的Node,加入队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//step2、获取当前node的前一个元素
final Node p = node.predecessor();
//step3、如果前一个元素是队首
if (p == head) {
//step4、再次调用tryAcquireShared()方法,判断state的值是否为0
int r = tryAcquireShared(arg);
//step5、如果state的值==0
if (r >= 0) {
//step6、设置当前node为队首,并尝试释放共享锁
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消
if (failed)
cancelAcquire(node);
}
}
按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。
1、首先看addWaiter()
//step1
private Node addWaiter(Node mode) {
//把当前线程封装为node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//获取当前队列的队尾tail,并赋值给pred
Node pred = tail;
//如果pred!=null,即当前队尾不为null
if (pred != null) {
//把当前队尾tail,变成当前node的前继节点
node.prev = pred;
//cas更新当前node为新的队尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队尾为空,走enq方法
enq(node);//step1.1
return node;
}
-----------------------------------------------------------------
//step1.1
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果队尾tail为null,初始化队列
if (t == null) { // Must initialize
//cas设置一个新的空node为队首
if (compareAndSetHead(new Node()))
tail = head;
} else {
//cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现
//step6
private void setHeadAndPropagate(Node node, int propagate) {
//获取队首head
Node h = head; // Record old head for check below
//设置当前node为队首,并取消node所关联的线程
setHead(node);
//
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前node的后继节点为null或者是shared类型的
if (s == null || s.isShared())
//释放锁,唤醒下一个线程
doReleaseShared();//step6.1
}
}
--------------------------------------------------------------------
//step6.1
private void doReleaseShared() {
for (;;) {
//找到头节点
Node h = head;
if (h != null && h != tail) {
//获取头节点状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒head节点的next节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
3、接下来我们来看countDown()方法。
public void countDown() {
sync.releaseShared(1);
}
可以看到调用的是父类AQS的releaseShared 方法
public final boolean releaseShared(int arg) {
//state-1
if (tryReleaseShared(arg)) {//step1
//唤醒等待线程,内部调用的是LockSupport.unpark方法
doReleaseShared();//step2
return true;
}
return false;
}
------------------------------------------------------------------
//step1
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
//获取当前state的值
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
//cas操作来进行原子减1
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
7、CountDownLatch内部实现:
CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。
下面我们通过一个简单的图来帮助我们理解一下:
7.1 await内部实现流程:
判断state计数是否为0,是,则放过执行后面的代码;
大于0,则表示需要阻塞等待计数为0
当前线程封装Node对象,进入阻塞队列
然后就是循环尝试获取锁,直到成功(即state为0)后出队,继续执行线程后续代码。
7.2 countDown内部实现流程:
尝试释放锁tryReleaseShared,实现计数-1
若计数已经小于0,则直接返回false
否则执行计数(AQS的state)减一
若减完之后,state==0,表示没有线程占用锁,即释放成功,然后就需要唤醒被阻塞的线程了
释放并唤醒阻塞线程 doReleaseShared
如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出
头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队。
7.3 countDown 与CyclicBarrier的区别:
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。
CountDownLatch一般用于某个线程等待若干个其他线程执行完任务之后,它才执行。
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
闭锁用于等待事件,而栅栏用于等待其他线程。
8、AbstractQueuedSynchronizer (简称AQS)
AQS是一个用于构建锁和同步容器的框架。
事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。
AQS解决了在实现同步容器时设计的大量细节问题
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。
其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//取值为 CANCELLED, SIGNAL, CONDITION, PROPAGATE 之一
volatile int waitStatus;
volatile Node prev;
volatile Node next;
// Link to next node waiting on condition,
// or the special value SHARED
volatile Thread thread;
Node nextWaiter;
}
9、CountDownLatch应用场景:
电商的详情页,由众多的数据拼装组成,如可以分成一下几个模块:
交易的收发货地址,销量
商品的基本信息(标题,图文详情之类的)
推荐的商品列表
评价的内容
....
上面的几个模块信息,都是从不同的服务获取信息,且彼此没啥关联;所以为了提高响应,完全可以做成并发获取数据,如
线程1获取交易相关数据
线程2获取商品基本信息
线程3获取推荐的信息
线程4获取评价信息
....
但是最终拼装数据并返回给前端,需要等到上面的所有信息都获取完毕之后,才能返回,这个场景就非常的适合 CountDownLatch来做了。
当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。
在这个需求中,要实现主线程等待所有线程完成sheet的解析操作。
参考博文:
https://blog.csdn.net/qq_38293564/article/details/80557355
http://blog.itmyhome.com/2017/07/java-concurrent-countdownlatch