常用并发工具类介绍

25 阅读7分钟

它们分别是:

  • 信号量 Semaphore
  • 倒计时门栓 CountDownLatch
  • 屏障 CyclicBarrier

所以,既然是工具类,那么必然是离不开特定的场景的,于是相互之间没有谁优谁劣,只有谁更合适。

1. 信号量 Semaphore

1.1 信号量原理

Semaphore又称"信号量",也是一个非常有用的工具类,它相当于是一个并发控制器,用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。Semaphore 内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。访问特定资源前,必须使用acquire()方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。访问资源后,使用release()方法释放许可。

Semaphore(int permits,boolean fair)提供了2个参数。permits 代表资源池的长度;fair 代表 公平许可 或 非公平许可。类似公平锁/非公平锁的概念。Semaphore中主要用到的是acquire() 和release()两个方法,分别用来获取信号量 和 释放信号量。

1.2 使用场景

Semaphore 适用于什么样的使用场景呢,我们举个通俗的例子:

假如现在有一个停车场,里面有只十个停车位,当着十个停车位都被占用了,外面的车就不允许进入了,就必须在外面等着。出来一辆车才允许进去一辆车

这个场景不同于我们一般的并发场景,一般来说,我们的临界资源只能允许一个线程进行访问,其他线程都地等着。

但是,有一种场景是,临界资源允许多个线程同时访问,超过限定数量的外的线程得阻塞等待

Semaphore 可以说是为上述这种场景而生的一个工具类,我们写个 demo 实现上述逻辑:

image

执行程序之后,你会看到: image

出来一个线程才允许进去一个线程,这就是 Semaphore。

semaphore 的内部原理其实你去看源码,你会发现和我们的 ReentrantLock 的实现是极其类似的,包括公平与非公平策略的支持,只不过,AQS 里面的 state 在前者的实现中,一般小于等于1(除非重入锁),而后者的 state 则小于等于10,记录的是剩余可用临界资源数量。

所以,semaphore 天生就存在一个问题,如果某个线程重入了临界区,可用临界资源的数量是否需要减少?

停车场一共十个停车位,一辆车进去并占有了一个停车位,过了一段时间,这个向管理员报告,我还要占用一个停车位,先不管他占两个干啥,此时的管理员会同意吗?

实际上,在 Java 这个管理员看来,已经进入临界区的线程是「老爷」,提出的要求都会优先满足,即便他自身占有的资源并没有释放。

所以,在 Semaphore 机制里,一个线程进入临界区之后占用掉所有的临界资源都是可能的

2. 倒计时门栓 CountDownLatch

2.1 原理

CountDownLatch是一个计数器闭锁。主要的功能就是:在完成一组线程中执行的操作之前,它允许一个或多个线程通过await()方法来阻塞处于一直等待状态,用给定的计数初始化CountDownLatch,调用countDown()方法计数减一,当计数器减少到0时,再唤起这些线程继续执行。常用于监听某些初始化操作,等待初始化执行完毕后,通知主线程继续工作。

CountDownLatch中主要用到的是 countDown()和await()这两个方法。await() 用于以执行完成任务的阻塞等待,使当前线程在计数为零之前一直阻塞。countDown() 递减计数,如果计数达到零,说明所有任务都执行完成。

它还提供了带参数的 await(long timeout,TimeUnit unit)方法,来指定其他线程等待的时长。如果超时还未完成,则直接跳出阻塞执行下面的流程。


2.2 使用场景

有这么一个常见的场景,我们一起来看看:

大家日常经常使用的拼多多,一件商品至少需要两到三人拼团,商家才会发货。

这里,我们不去研究它的商业模式,不管他是怎么实现盈利的,就这么一种场景,如果要用基本的并发 API 来实现,你可能会想到:

来一个线程阻塞一次,知道达到指定的数量后,全部唤醒

对,没错,CountDownLatch 内部就是这样实现的,轮子已经帮你造好了,我们来看看该怎么实现上述的模型案例: image

多运行几次,你会发现结果不会错,拼团的人先后顺序可能不同,但商家一定是在三个人都准备好了之后才会发货。

除此之外,它还有更多的应用,比如百米赛跑,只有当所有运动员都准备好了之后,裁判员才会吹响哨子,等等等等。

实现原理也基本和显式锁类似,不同点依然在于对 state 的控制,CountDownLatch 只判断 state 是否等于零,不等于零就说明时机未到,阻塞当前线程。

而每一次的 countDown 方法调用都会减少一次倒计时资源,直至为零才唤醒阻塞的线程。

3. 循环屏障 CyclicBarrier

3.1 原理

CyclicBarrier是一个同步辅助类,它允许一组数据线程相互等待,直到所有线程都到达一个公共的屏障点; 在程序中有固定的数量的线程,这些线程有时候必须等待彼此,这种情况下,使用CyclicBarrier很有帮助; 这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的

3.2 使用场景

考虑这么一个场景:

公寓的班车总是在公寓楼下装满一车人之后,出发并开到地铁站,接着再回来接下一班人。

这么一个场景,我们考虑该怎么实现:

image 效果大概就是这个样子:

image

CyclicBarrier 就像一个屏障,实例化的时候需要传入两个参数,第一个参数指定我们的屏障最多拦截多少个线程后就打开屏障,第二个参数指明最后一个到达屏障的线程需要额外做的操作。

一般而言,最后一个线程到达屏障后,屏障将会打开,释放前面所有的线程,并在最后重新关上屏障。

CyclicBarrier 只需要用到一个 await 就可以完成所有的功能,我们总结下该方法的实现逻辑:

  1. 首先,减少一次可用资源数量
  2. 如果可用资源数为零,则说明自己是最后一个线程,于是会执行我们传入的额外操作,唤醒所有已经到达在等待的线程,并重新开启一个屏障计数。
  3. 否则说明自己不是最后一个线程,于是将自身线程在一个循环当中阻塞到一个条件队列上

4. CountDownLatch和CyclicBarrier区别

第一个区别

倒计时门栓 CountDownLatch 一旦被打开后就不能再次合上,也是说只要被调用了足够次数的 countDown,await 方法就会失效,它是一次性的。

CyclicBarrier 是循环发生的,当最后一个线程到达屏障,会优先重置屏障计数,屏障再次开启拦截阻隔。

第二个区别

CountDownLatch 是计数器, 线程来一个就记一个,此期间不阻塞线程,当达到指定数量之后才会去唤醒外部等待的线程,也就是说外部是有一个乃至多个线程等待一个条件满足之后才能继续执行,而这个条件就是满足一定数量的线程,这样才能激活当前外部线程的继续执行。

tch 是计数器, 线程来一个就记一个,此期间不阻塞线程,当达到指定数量之后才会去唤醒外部等待的线程,也就是说外部是有一个乃至多个线程等待一个条件满足之后才能继续执行,而这个条件就是满足一定数量的线程,这样才能激活当前外部线程的继续执行。

CyclicBarrier 像一个栅栏,来一个线程阻塞一个,直到阻塞了指定数量的线程后,一次性全部激活,让他们同时执行,像一个百米冲刺一样。