目前卷文化盛行,为了增强面试能力,开始了无边无际的学习,无边界不是重点,重点是要深入1万米。言归正传,本文打算做一个多线程学习的系列文章,沉淀自我。
前言
本文主要是讲解CountDownLatch的概念,基本用法,使用场景,底层代码原理剖析。
一、CountDownLatch是什么?
按照源码注释,翻译成中文,就是以下内容:
- 允许一个或多个线程等待其他线程正在执行的一组操作完成。
- await方法阻塞当前线程直到由于countDown方法的调用将当前计数为0。
- 计数为0之后,所有等待的线程被释放和任何后续调用的await立即返回。
- 这是一个一次性的现象——计数不能重置。如果你需要重置计数的版本,考虑使用CyclicBarrier循环障碍。
- 中文叫法很多,如 倒计时门栓,发令枪,闭锁,计数器。
二、使用方法
CountDownLatch 提供了一些方法:
方法 | 说明 |
---|---|
await() | 使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。 |
await(long timeout, TimeUnit unit) | 带超时时间的await()。 |
countDown() | 使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。 |
getCount() | 获得latch的数值。 |
三、应用场景
倒计时门栓是一个多用途的同步工具,可以用于许多目的。
初始化作为一个门或者门栓的开关。当所有线程调用await在门口等待,直到一个线程调用countDown且count等于0而被打开。
计数初始化为N,可以用来使一个线程等待N线程完成了一些行动,或一些行动已经完成N次。线程调用wait时,不需要计数达到零,它只是阻止任何线程继续运行await程序之后,直到所有线程可以通过。
3.1双门栓
首先是一个开始的信号,防止任何工人进行,直到司机准备继续;
第二是完成信号,允许司机等到所有工人已经完成了。
代码如下
package com.valley.juc.tools.countdownlatch;
import java.util.concurrent.CountDownLatch;
/**
* @author valley
* @date 2022/6/15
* @Description 多门栓demo
*/
public class Driver {
public static void main(String[] args) throws InterruptedException {
int N =100;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) { // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
}
new Driver().doSomethingElse(); // don't let run yet
startSignal.countDown(); // let all threads proceed
doSomethingElse2();
doneSignal.await(); // wait for all to finish
}
private static void doSomethingElse2() {
System.out.println("doSomethingElse...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doSomethingElse(){
synchronized (this){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("don't let run yet");
}
static class Worker implements Runnable{
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
@Override
public void run() {
try {
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void doWork() {
System.out.println("worker doWork...");
}
}
}
3.2单门栓
模拟并发,让并发线程一起执行。在并发请求HTTP接口的时候,合并多个返回结果,都是就是这种应用场景。
代码如下:
package com.valley.juc.tools.countdownlatch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author valley
* @date 2022/6/15
* @Description TODO
*/
public class Driver2 {
public static void main(String[] args) throws InterruptedException {
int N = 100;
CountDownLatch doneSignal = new CountDownLatch(N);
// Executor e = Executors.newFixedThreadPool(2);
List<String> list = new ArrayList<>();
for (int i = 0; i < N; ++i) { // create and start threads
// e.execute(new WorkerRunnable(doneSignal, i, list));
new Thread(new WorkerRunnable(doneSignal, i, list)).start();
}
doneSignal.await(); // wait for all to finish
AtomicReference<Integer> count = new AtomicReference<>(0);
list.forEach(s -> {
count.set(count.get() + Integer.parseInt(s));
});
System.out.println(count.get());
}
static class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
private final List<String> list;
public WorkerRunnable(CountDownLatch doneSignal, int i, List<String> list) {
this.doneSignal = doneSignal;
this.i = i;
this.list = list;
}
@Override
public void run() {
doWork(i);
list.add("100");
doneSignal.countDown();
}
private void doWork(int i) {
System.out.println("i'm " + i);
}
}
}
四、源码剖析
CountDownLatch 的源码在JUC并发工具中,相对简单:
- 静态内部类继承AQS抽象类,所以底层基于AbstractQueuedSynchronizer 来实现的
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;
}
}
}
- CountDownLatch 构造函数中指定的count直接赋给AQS的state;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
- 每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。
public void countDown() {
sync.releaseShared(1);
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
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
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
- 而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
五、和其他工具比较
- CountDownLatch与Thread.join
CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减1操作,也可以在一个线程里调用n次执行减1操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
- CountDownLatch与CyclicBarrier
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,区别如下:
CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后再执行,而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。
总结
主要讲解CountdownLatch,后面重点讲AQS。