在JDK的并发包(JUC)里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Samaphore工具类提供了一种并发流程控制的手段,这同样也是面试和工作中的一个重要知识点,本文将从它们的定义、常用方法、代码示例及核心源码的分析等几个要点详细介绍一下。
CountDownLatch是一个计数器,它允许一个或多个线程等待其它线程完成操作后再继续执行,通常用来实现一个线程等待其它多个线程完成操作之后再继续执行的操作。
CountDownLatch内部维护了一个计数器,该计数器通过CountDownLatch的构造方法指定。当调用await()方法时,它将一直阻塞,直到计数器变为0。当其它线程执行完指定的任务后,可以调用countDown()方法将计数器减一。当计数器减为0,所有的线程将同时被唤醒,然后继续执行。
举个例子——王者5V5模式:当10个玩家都选完英雄后会进入加载界面,直到10个人都加载到100%,才会真正进入游戏。
CountDownLatch(int count)
:CountDownLatch的构造方法,可通过count参数指定计数次数,但是要大于等于0,小于0会抛IIegalArgumentException异常。void await()
:在计数等于0之前,会一直阻塞(在线程没被打断的情况下)。boolean await(long timeout,TimeUnit unit)
:除非线程被中断,否则会一直阻塞,直至计数器减为0或超出指定时间timeout,当计数器为0返回true,当超过指定时间,返回false。void countDown()
:调用一次,计数器就减1,当等于0时,释放所有线程。如果计数器的初始值就是0,那么就当没有用CountDownLatch吧。long getCount()
:返回当前计数器的数量,可以用来测试和调试。
首先我们创建一个Worker类,并使用CountDownLatch来控制它执行多少次后放行,output变量充当运行日志来记录运行轨迹:
@AllArgsConstructor
@NoArgsConstructor
public class Worker implements Runnable{
private List output;
private CountDownLatch countDownLatch;
@Override
public void run() {
//do something
countDownLatch.countDown();
output.add("CountDown减一,剩余:"+countDownLatch.getCount());
}
}
接下来测试一下:
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException {
List output = Collections.synchronizedList(new ArrayList<>());
CountDownLatch countDownLatch=new CountDownLatch(5);
List workers = Stream.generate(() -> new Thread(new Worker(output, countDownLatch)))
.limit(5)
.collect(Collectors.toList());
workers.forEach(Thread::start);
//阻塞
countDownLatch.await();
output.add("计数器减为"+countDownLatch.getCount()+",放行了!!!");
for (String s : output) {
System.out.println(s);
}
}
}
执行结果:
CountDown减一,剩余:4
CountDown减一,剩余:3
CountDown减一,剩余:2
CountDown减一,剩余:1
CountDown减一,剩余:0
计数器减为0,放行了!!!
很显然,“计数器减为0,放行了!!!”这句话总是最后一个输出,因为它依赖于CountDownLatch的释放。
CountDownLatch的实现原理主要时通过内部类Sync类实现的,而内部类Sync是AQS的子类,通过重写AQS的共享式获取和释放同步状态方法实现的。如下:
ps:下面逐个介绍CountDownLatch的核心方法时会解释。
先来看一下CountDownLatch的构造方法,对应Sync类的第一个红框。
解释:初始化CountDownLatch实际就是设置了AQS的state为计数的值。
对应Sync类的第二个红框。
解释:获取AQS的state值。
对应Sync类的第三个红框。
解释:调用await方法实际就是调用AQS的共享式获取同步状态的方法,acquireSharedInterruptibly方法里调用了Sync类的tryAcquireShared方法,该方法返回值小于0代表计数器还未减为0,那么继续调用doAcquireSharedInterruptibly方法。
里面时一个死循环for,跳出死循环的条件就是Sync类的tryAcquireShared方法返回值大于等于0,也就是计数器减为0了,否则将一直处在死循环中,也就是会一直阻塞。
对应Sync类的第四个红框。
解释:调用CountDownLatch的countDown方法实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state的值。
CountDownLatch实际完全依靠AQS的共享式获取和释放同步状态来实现,初始化时定义AQS的state值,每调用countDown实际就是释放一次AQS的共享式同步状态,await方法实际就是尝试获取AQS的同步状态,只有当同步状态值为0时才能获取成功。
CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。
CyclicBarrier也维护了一个类似计数器的变量,通过CyclicBarrier的构造函数指定,需要大于0,否则抛IllegalArgumenException异常。当线程到达屏障位置时,调用await()方法进行阻塞,直到所有线程到达屏障位置时,所有线程才会被释放,而屏障将会被重置为初始值以便下次使用。
举个例子——王者五排:五个人都到齐后,才能开始匹配。
CyclicBarrier(int parties)
:CyclicBarrier的构造方法,可通过parties参数指定需要到达屏障的线程个数,但是要大于0,否则会抛IllegalArgumentException异常。CyclicBarrier(int parties,Runnable barrierAction)
:另一个构造方法,parties作用同上,barrierAction表示最后一个到达屏障点的线程要执行的逻辑。int await()
:表示线程到达屏障点,并等待其它线程到达,返回值表示当前线程在屏障中的位置(第几个到达的)。int await(long timeout,TimeUnit unit)
:与await()类似,但是设置了超时时间,如果超过指定的时间后,仍然还有线程没有到达屏障点,则等待的线程会被唤醒并执行后续操作。void reset()
:重置屏障状态,即将屏障计数器重置为初始值。int getParties()
:获取需要同步的线程数量。int getNumberWaiting()
:获取当前正在等待的线程数量。
假如有一个由固定数量线程执行的操作,并将相应的结果存到一个列表中。当所有线程完成它们的操作时,其中一个线程开始处理每个线程获取到的数据。
@AllArgsConstructor
@NoArgsConstructor
public class CyclicBarrierDemo {
private CyclicBarrier cyclicBarrier;
//存储每个工作线程的结果
private List results = Collections.synchronizedList(new ArrayList<>());
private Random random = new Random();
//每个工作线程要产生的结果数量
private int resultNums;
//要执行的线程数量
private int workerNums;
//每个线程的执行逻辑
class AcquireNums implements Runnable {
@Override
public void run() {
String thisThreadName = Thread.currentThread().getName();
//Random产生随机数并存入
for (int i = 0; i < resultNums; i++) {
Integer num = random.nextInt(10);
System.out.println(thisThreadName + ": 随机结果 - " + num);
results.add(num);
}
try {
System.out.println(thisThreadName + " 等待其它线程到达屏障点.");
//阻塞其它线程
cyclicBarrier.await();
} catch (Exception e) {
}
}
}
//所有线程到达屏障点时执行的操作
class AfterThreadWork implements Runnable {
@Override
public void run() {
String thisThreadName = Thread.currentThread().getName();
int sum = 0;
//计算结果
for (Integer result : results) {
System.out.println("加"+result);
sum += result;
}
System.out.println(thisThreadName + ": 总和 = " + sum);
}
}
//程序入口
public void runSimulation(int numWorkers, int numberOfresults) {
resultNums = numberOfresults;
workerNums = numWorkers;
cyclicBarrier = new CyclicBarrier(workerNums, new AfterThreadWork());
System.out.println("创建" + workerNums + "个线程,每个线程获取" + resultNums + "个结果");
for (int i = 0; i < workerNums; i++) {
Thread worker = new Thread(new AcquireNums());
worker.setName("Thread " + i);
worker.start();
}
}
public static void main(String[] args) {
CyclicBarrierDemo demo = new CyclicBarrierDemo();
demo.runSimulation(5, 3);
}
}
上述代码使用5个线程初始化了CyclicBarrier,每个线程都会产生3个随机整数,并将其存储在结果集合里,当所有的线程都到达屏障点时,最后一个到达的将会执行AfterThreadWork中的逻辑,即计算集合里数字的总和。
执行结果:
创建5个线程,每个线程获取3个结果
Thread 0: 随机结果 - 7
Thread 0: 随机结果 - 0
Thread 0: 随机结果 - 8
Thread 0 等待其它线程到达屏障点.
Thread 1: 随机结果 - 1
Thread 3: 随机结果 - 5
Thread 2: 随机结果 - 8
Thread 2: 随机结果 - 6
Thread 3: 随机结果 - 5
Thread 4: 随机结果 - 5
Thread 1: 随机结果 - 1
Thread 4: 随机结果 - 4
Thread 3: 随机结果 - 0
Thread 3 等待其它线程到达屏障点.
Thread 2: 随机结果 - 4
Thread 2 等待其它线程到达屏障点.
Thread 4: 随机结果 - 8
Thread 4 等待其它线程到达屏障点.
Thread 1: 随机结果 - 0
Thread 1 等待其它线程到达屏障点.
Thread 1: 总和 = 62
主要有两个属性:parties(总线程数)、count(当前剩余线程数),需要两个值的维护的原因是CyclicBarrier提供了重置的功能,当调用reset方法时,需要将count的值再次重置为parties的初始值。
ps:下面多处会提到。
解释:用于中断屏障操作。
- 将broken置为true。
- count的值置为parties初始值。
- 调用Condition的singnalAll方法唤醒所有线程。
ps:下面多处会提到。
解释:作用是完成上一代的屏障操作,为下一代屏障操作做准备。
- 调用Condition的signalAll方法唤醒所有线程。
- count的值置为parties初始值。
- 创建新的Generation实例,作为下一代表示。
可以看到await方法里面调用了dowait方法,下面我们看一下dowait方法(dowait的方法有点长,分两次分析)
解释:
第一个红框:用ReentrantLock加锁,防止出现并发问题。
第二个红框:
Generation是CyclicBarrier的一个内部类,里面仅有一个boolean类型的broken属性(初始值为false)。此处的作用是检查broken是否为ture,若为true,则抛异常,这就意味着当前屏障已经被中断或重置了,避免了继续等待。
第一个if:判断屏障是否失效,若失效,抛异常。
第二个if:如果线程被中断,那么调用breakBarrier方法直接中断屏障,并抛出异常。
第三个红框:
计数器减一。
判断所有线程是否都到达屏障。
如果index==0,则执行屏障操作。如果有需要优先执行的任务(CyclicBarrier构造方法的第二个参数是否为空),则执行run()方法。调用nextGeneration方法为下一代做准备。
finally:如果不能执行屏障操作(ranAction为false),则调用breakBarrier方法中止屏障操作。
接着第二段源码:
解释:如果计数器没减到0,就让当前线程进入到等待队列中等待
第一个红框:
timed=ture表示设置了超时等待时间,timed=false表示没设置。没设置的话就调用Condition的await()方法进入到等待队列。nacos表示超时时间,大于0表示设置了超时时间,那么就会调用Condition的awaitNanos方法在限定的时间内等待。
如果等待过程中发生了中断interruptedException,进入catch逻辑:如果g是当前Generation并且broken为false,则调用breakBarrier方法终止屏障操作,并抛出异常。反之,说明Genertion已经不是最新的
第二个红框:
第1个if:broken为ture,表示屏障已被中断或重置,抛出异常。
第2个if:如果g!=Generation,说明当前Generation不是最新的了,则返回当前线程在屏障上等待的位置index。
第3个if:如果设置了超时等待且已经超时,则调用breakBarrier方法终止屏障操作,并抛出异常。
解释:重置屏障,将其恢复至初始状态
- 加锁。
- 获取锁后调用breakBarrier方法终止屏障操作。
- 接着调用nextGeneration方法为下一代屏障操作做准备。
从源码可以看出CyclicBarrier的实现原理主要是通过ReentranLock和Condition来实现的,主要的流程如下:
- 创建CyclicBarrier时指定需要到达屏障的线程数parties。
- 当调用await方法时,会首先通过ReentranLock进行加锁,然后对count进行自减操作。
- 如果计数器没减为0,则调用Condition的await或awaitNanos方法使当前线程进入等待状态。
- 如果计数器减为0了,表示全部抵达屏障,此时就调用nextGeneration方法为下一代屏障操作做准备。
思考:既然CycliBarrier可以用ReentranLock+Condition实现,那么Synchronized+wait/notify是否也可以实现CyclicBarrier?
ps:关于锁,可以参考另外一篇文章:Java并发编程第4讲——Java中的锁(万字详解)
趁热打铁,介绍一下它俩的区别,最后再介绍Samphore(信号量)。
- CountDownLatch:主要作用于一个或多个线程等待其它线程完成一组操作,计数器为0时,会唤醒所有的等待线程。
- CyclicBarrier:主要用于一组线程互相等待,直到所有线程都到达屏障后才继续执行,之后CyclicBarrier可以被重用。
根据上述功能,我们可以知道CyclicBarrier允许一组线程互相等待,而CountDownLatch允许一个或多个线程等待一些任务完成。
说白了,就是CyclicBarrier维护线程的计数,而CounDownLatch维护任务的计数。
上代码:
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(2);
new Thread(()->{
countDownLatch.countDown();
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println(countDownLatch.getCount());//0
}
}
注意:我们用了一个线程执行了两次countDown操作,结果计数器为0。我们再来看看CyclicBarrier:
public class Test {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier=new CyclicBarrier(2);
new Thread(()->{
try {
cyclicBarrier.await();
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
Thread.sleep(100);
//当前正在屏障点等待的线程
System.out.println(cyclicBarrier.getNumberWaiting());//1
System.out.println(cyclicBarrier.isBroken());//false
}
}
可以看到,单个线程无法两次减少屏障的计数,第二个await是无用的,还有就是broken为false,说明还在本Generation中。
CountDownLatch和CyclicBarrier最明显的差异就是可重用性。CyclicBarrier所有线程都到达屏障后,计数会重置为初始值。而CountDownLatch永远不会重置。
上代码:定义一个计数器为5的CountDownLatch,通过10次不同的调用计数
public class TestNums {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(5);
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
//当前计数器的数量
long count = countDownLatch.getCount();
countDownLatch.countDown();
if(countDownLatch.getCount() != count){
list.add("计数器次数变了");
}
}).start();
}
Thread.sleep(100);
System.out.println(list.size());//5
}
}
上述代码,可以看出,每次一个线程运行时,值都会减少,一旦等于0,计数器也不会重置。
再来看看CyclicBarrier:
public class TestNums {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier=new CyclicBarrier(5,()->{
System.out.println("所有线程都到达屏障!!!");
});
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
输出:
所有线程都到达屏障!!!
所有线程都到达屏障!!!
可以看到所有线程到达屏障后,需要优先执行的逻辑执行了两次,说明,完成了两次所有线程到达屏障的操作,也就是说CycliBarrier的计数器一旦为0,便会将计数器重置为初始值。
Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成的操作。
Semaphore维护了一个内部计数器(许可permits),主要有两个操作,分别对应Semaphore的acquire和release方法。acquire方法用于获取资源,当计数器大于0时,将计数器减1;当计数器等于0时,将线程阻塞。release方法用于释放资源,将计数器加1,并唤醒一个等待中的线程。
举个例子:假设停车场有5个车位,如果同时来了6辆车,那么保安只允许5辆车进入(获取许可),剩下的一辆车只能等待它们其中一辆开走(释放许可),才能进入。
Semaphore(int permits)
:构造方法,permits表示Semaphore中的许可数量,它决定了同时可以访问某个资源的线程数量。Semaphore(int permits,boolean fair)
:构造方法,当fair为ture,设置为公平信号量。void acquire()
:获取一个许可,如果没有许可,则当前线程被阻塞,直到有许可。如果有许可该方法会将许可数量减1。void acquire(int permits)
:获取指定数量的许可,获取成功同样将许可减去指定数量,失败阻塞。void release()
:释放一个许可,将许可数加1。如果有其他线程正在等待许可,则唤醒其中一个线程。void release(int n)
:释放n个许可。int availablePermits()
:当前可用许可数。
假设停车厂有固定的停车车位,每次停车和离开都会占有或释放一个车位:
public class SemaphoreTest {
private Semaphore parking;
//初始化许可数量
public SemaphoreTest(int n){
parking=new Semaphore(n);
}
//停车
public void park(){
try {
//获取一个停车位,如果没有则阻塞
parking.acquire();
System.out.println(Thread.currentThread().getName()+" 停车成功!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//释放车位
public void leave(){
//释放一个车位
parking.release();
System.out.println(Thread.currentThread().getName()+" 离开了停车场!");
}
public static void main(String[] args) {
//初始化3个停车位
SemaphoreTest parking=new SemaphoreTest(3);
//假设此时有5辆车需要停放
for (int i = 0; i < 5; i++) {
new Thread(()->{
//停车
parking.park();
try {
//假设每辆车停留300毫秒
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//离开停车厂
parking.leave();
},"Car-"+i).start();
}
}
}
上述代码难度不大,就不做解释了,来看下结果吧:
Car-0 停车成功!
Car-1 停车成功!
Car-2 停车成功!
Car-1 离开了停车场!
Car-4 停车成功!
Car-0 离开了停车场!
Car-2 离开了停车场!
Car-3 停车成功!
Car-3 离开了停车场!
Car-4 离开了停车场!
解释:
- 构造方法有两个参数,permits是许可证的数量,fair表示是否开启公平模式,模式是非公平模式。
Semaphore的实现同样也是通过其内部类Sync类来实现的,Sync类也是AQS的子类。读到这不知道你想起ReentrantLock了没,没错,Semaphore的实现原理基本上和ReentrantLock如出一辙。
遵循FIFO原则,先排队的线程先拿到许可证。
可以看到acquire方法调用了acquireSharedInterruptible方法,而acquireSharedInterruptible方法里有一个tryAcquireShared方法的if判断,这里先说一下,tryAcquireShared方法的返回值小于0表示获取许可失败,失败的情况下会调用doAcquireSharedInterrutpible方法对线程进行阻塞,下面我们先看一下tryAcquireShared方法的源码。
解释:
- 检查是否有其他线程在当前线程之前排队等待,如果有,则意味着当前线程无法获取资源,直接返回-1。
- 计算剩余资源数量(remaining),即当前资源数量-需要获取的资源数量。接着判断remaining是否小于0或使用compareAndSetState方法尝试将当前资源状态更新位剩余资源数量。如果小于0或cas成功,返回剩余资源数量;反之则不断循环重试。
ps:remain小于0说明当前许可不够,因此需要立即返回(这个值是负数),这是一种快速失败的策略。到了cas的判断的时候就说明许可数量足够,只需要判断更新是否成功。
最后看一下doAcquireSharedInterrutpible方法
可以看到这又是一个死循环,里面又调了tryAcquireShared方法不断尝试获取许可,只有成功或许许可(返回值大于0)才跳出循环,也就是解除阻塞。
非公平就谁拿到资源谁用,不分先后。
只是少了一步检查是否有其他线程在当前线程之前排队等待的判断而已,剩余的也都一样,这里就不做过多的介绍了。
release方法调用了父类AQS的releaseShared方法,releaseShared方法里,调用Sync类的tryReleaseShared方法做了条件判断,如果为ture就调用AQS父类的doReleaseShared方法,接着返回ture表示释放许可成功,反之返回false表示释放许可失败。下面我们来看下这两个方法的源码。
tryReleaseShared():
解释:
- 当前许可数量+需要释放的许可数量得到释放后许可的数量next。
- 如果next小于当前许可的数量,说明许可数发生了溢出,就抛异常。
- 如果next大于等于当前许可的数量,尝试将当前许可数更新为next,成功返回ture,如果都不满足就继续循环,直到尝试成功。
如果释放成功,则调用AQS父类的doReleaseShared方法:
解释:简单的说就是更新许可数量成功后,就会唤醒等待队列中的其它线程去竞争来获取许可。(此处是AQS的源码,这里就简单解释一下,后续会总结AQS的文章)
Semaphore主要用于控制当前活动线程数目,就像上面停车场的例子,Semaphore就相当于停车场看管员,主要的职责就是控制车辆和车位,而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()方法释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教。