三个好用的并发工具类
三个好用的并发工具类
以前的文章中,我们介绍了太多的底层原理技术以及新概念,本篇我们轻松点,了解下 Java 并发包下、基于这些底层原理的三个框架工具类。
它们分别是:
- 信号量 Semaphore
- 倒计时门栓 CountDownLatch
- 屏障 CyclicBarrier
所以,既然是工具类,那么必然是离不开特定的场景的,于是相互之间没有谁优谁劣,只有谁更合适。
信号量 Semaphore
Semaphore 适用于什么样的使用场景呢,我们举个通俗的例子:
假如现在有一个停车场,里面有只十个停车位,当着十个停车位都被占用了,外面的车就不允许进入了,就必须在外面等着。出来一辆车才允许进去一辆车
这个场景不同于我们一般的并发场景,一般来说,我们的临界资源只能允许一个线程进行访问,其他线程都地等着。
但是,有一种场景是,临界资源允许多个线程同时访问,超过限定数量的外的线程得阻塞等待。
这种情境使用原始的那一套也是能实现的,但那叫「造轮子」,Java 并发框架下给我们提供了一个工具类,专门适用这种场景。
Semaphore 可以说是为上述这种场景而生的一个工具类,我们写个 demo 实现上述逻辑:
package cn.zytao.taosir.concurrent; import java.util.concurrent.Semaphore; public class SemaphoreDemo { public static void main(String[] args) { //十个停车位 Semaphore semaphore = new Semaphore(10, false); while(true) { new Thread(()->{ try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"进入停车场,占用一个停车位."); //模拟停车时长 Thread.sleep((long)((int)(1+Math.random()*10)*1000)); semaphore.release(); System.out.println(Thread.currentThread().getName()+"离开停车场..."); }catch(InterruptedException e) { e.printStackTrace(); } }).start(); } } }
执行程序之后,你会看到:
你看,出来一个线程才允许进去一个线程,这就是 Semaphore。
semaphore 的内部原理其实你去看源码,你会发现和我们的 ReentrantLock 的实现是极其类似的,包括公平与非公平策略的支持,只不过,AQS 里面的 state 在前者的实现中,一般小于等于一(除非重入锁),而后者的 state 则小于等于十,记录的是剩余可用临界资源数量。
所以,semaphore 天生就存在一个问题,如果某个线程重入了临界区,可用临界资源的数量是否需要减少?
停车场一共十个停车位,一辆车进去并占有了一个停车位,过了一段时间,这个向管理员报告,我还要占用一个停车位,先不管他占两个干啥,此时的管理员会同意吗?
实际上,在 Java 这个管理员看来,已经进入临界区的线程是「老爷」,提出的要求都会优先满足,即便他自身占有的资源并没有释放。
所以,在 Semaphore 机制里,一个线程进入临界区之后占用掉所有的临界资源都是可能的。
倒计时门栓 CountDownLatch
下面我们来看看这个 CountDownLatch,名字听起来挺高级,究竟提供了怎样的功能呢?
有这么一个常见的场景,我们一起来看看:
大家日常经常使用的拼多多,一件商品至少需要两到三人拼团,商家才会发货。
这里,我们不去研究它的商业模式,不管他是怎么实现盈利的,就这么一种场景,如果要用基本的并发 API 来实现,你可能会想到:
来一个线程阻塞一次,知道达到指定的数量后,全部唤醒
对,没错,CountDownLatch 内部就是这样实现的,轮子已经帮你造好了,我们来看看该怎么实现上述的模型案例:
package cn.zytao.taosir.concurrent; import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) { //假设一个商品需要3个人拼团 CountDownLatch latch=new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(()->{ try { //模拟拼团等待 Thread.sleep((long)(Math.random()*1000)); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"拼团成功..."); latch.countDown(); }).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("三人拼团成功,商家发货"); } }
多运行几次,你会发现结果不会错,拼团的人先后顺序可能不同,但商家一定是在三个人都准备好了之后才会发货。
除此之外,它还有更多的应用,比如百米赛跑,只有当所有运动员都准备好了之后,裁判员才会吹响哨子,等等等等。
实现原理也基本和显式锁类似,不同点依然在于对 state 的控制,CountDownLatch 只判断 state 是否等于零,不等于零就说明时机未到,阻塞当前线程。
而每一次的 countDown 方法调用都会减少一次倒计时资源,直至为零才唤醒阻塞的线程。
循环屏障 CyclicBarrier
CyclicBarrier 其实和 CountDownLatch 很像,我们先介绍完 CyclicBarrier,然后再和你一起去比较比较他俩的区别和相似点。
考虑这么一个场景:
公寓的班车总是在公寓楼下装满一车人之后,出发并开到地铁站,接着再回来接下一班人。
这么一个场景,我们考虑该怎么实现:
package cn.zytao.taosir.concurrent; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { static Integer count=0; public static void main(String[] args) { CyclicBarrier barrier=new CyclicBarrier(20,()->{ if (count==0) { System.out.println("班车准备开始运营~"); count++; } else { System.out.println("车上座位已经坐满了,请等待下一班..."); count++; } }); //公寓有一百人 for (int i = 0; i < 100; i++) { new Thread(()->{ try { //模拟起床耗时 Thread.sleep((long)(Math.random()*1000)); barrier.await(); System.out.println(Thread.currentThread().getName()+"赶上了第"+count+"趟班车..."); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
效果大概就是这个样子:
班车准备开始运营~ Thread-80赶上了第1趟班车... Thread-53赶上了第1趟班车... Thread-89赶上了第1趟班车... Thread-79赶上了第1趟班车... Thread-39赶上了第1趟班车... Thread-0赶上了第1趟班车... Thread-81赶上了第1趟班车... Thread-71赶上了第1趟班车... Thread-92赶上了第1趟班车... Thread-11赶上了第1趟班车... Thread-12赶上了第1趟班车... Thread-88赶上了第1趟班车... Thread-4赶上了第1趟班车... Thread-1赶上了第1趟班车... Thread-90赶上了第1趟班车... Thread-62赶上了第1趟班车... Thread-47赶上了第1趟班车... Thread-30赶上了第1趟班车... Thread-66赶上了第1趟班车... Thread-43赶上了第1趟班车... 车上座位已经坐满了,请等待下一班... Thread-24赶上了第2趟班车... Thread-59赶上了第2趟班车... Thread-18赶上了第2趟班车... Thread-46赶上了第2趟班车... Thread-35赶上了第2趟班车... Thread-78赶上了第2趟班车... Thread-99赶上了第2趟班车... Thread-83赶上了第2趟班车... Thread-45赶上了第2趟班车... Thread-95赶上了第2趟班车...
CyclicBarrier 就像一个屏障,实例化的时候需要传入两个参数,第一个参数指定我们的屏障最多拦截多少个线程后就打开屏障,第二个参数指明最后一个到达屏障的线程需要额外做的操作。
一般而言,最后一个线程到达屏障后,屏障将会打开,释放前面所有的线程,并在最后重新关上屏障。
CyclicBarrier 只需要用到一个 await 就可以完成所有的功能,我们总结下该方法的实现逻辑:
- 首先,减少一次可用资源数量
- 如果可用资源数为零,则说明自己是最后一个线程,于是会执行我们传入的额外操作,唤醒所有已经到达在等待的线程,并重新开启一个屏障计数。
- 否则说明自己不是最后一个线程,于是将自身线程在一个循环当中阻塞到一个条件队列上
好了,看完 CyclicBarrier 你会发现,它真的很类似我们的倒计时门栓,下面我们就来阐述他俩的区别与联系。
第一个区别
倒计时门栓 CountDownLatch 一旦被打开后就不能再次合上,也是说只要被调用了足够次数的 countDown,await 方法就会失效,它是一次性的。
CyclicBarrier 是循环发生的,当最后一个线程到达屏障,会优先重置屏障计数,屏障再次开启拦截阻隔。
第二个区别
CountDownLatch 是计数器, 线程来一个就记一个,此期间不阻塞线程,当达到指定数量之后才会去唤醒外部等待的线程,也就是说外部是有一个乃至多个线程等待一个条件满足之后才能继续执行,而这个条件就是满足一定数量的线程,这样才能激活当前外部线程的继续执行。
CyclicBarrier 像一个栅栏,来一个线程阻塞一个,直到阻塞了指定数量的线程后,一次性全部激活,让他们同时执行,像一个百米冲刺一样。
最后的最后
好了,以上就是我们 Java 并发包下面比较好用的三个工具类,其中前两个的底层实现几乎完全依赖显式锁的原理方法,后一个则是使用的显式锁加条件变量重新造的轮子,都是非常好用的工具!
作者:涛先森の日常
出处:https://www.cnblogs.com/it-taosir/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。