java架构之路(多线程)JUC并发编程之Semaphore信号量、CountDownLatch、CyclicBarrier栅栏、Executors线程池
上期回顾:
上次博客我们主要说了我们juc并发包下面的ReetrantLock的一些简单使用和底层的原理,是如何实现公平锁、非公平锁的。内部的双向链表到底是什么意思,prev和next到底是什么,为什么要引入heap和tail来值向null的Node节点。高并发时候是如何保证state来记录重入锁的,在我们的上次博客都做了详细的说明。这次我们来聊一些简单易懂且实用的AQS中的工具类。
Semaphore信号量:
这个东西很简单,别看字面意思,什么信号量,我也不懂得那个术语什么意思,Semaphore你可以这样来理解,我们要去看电影,而且是3D电影(必须戴3D眼镜才可以进入),但是比较不巧的是我们电影院只有两个3D眼镜了,也就是说,我们每次只能进去两个人看电影,然后等待这两个人看完电影以后把眼镜还回来,后面的两个人才能继续观看,就是说每次只允许最多进去两个人,每次进入到线程获取锁,需要你得到前置的票据,才可以进行后续的流程。可以理解为一个简单的限流吧。我们来一下代码示例。
public class Test {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(new Task(semaphore,"xiaocaijishu"+i)).start();
}
}
static class Task extends Thread{
Semaphore semaphore;
public Task(Semaphore semaphore,String tname){
this.semaphore = semaphore;
this.setName(tname);
}
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿着3D眼镜进去了,时间是"+System.currentTimeMillis());
Thread.sleep(1000);
semaphore.release();
System.out.println(Thread.currentThread().getName()+"出来了,将3D眼镜还给了服务人员,时间是"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果就是这样的

我们来解释一下运行结果,线程1和线程3同一时间去看电影了,然后1出来了,这时线程9马上拿着我们的3D眼镜进去了,过了一会线程3也看完电影了,出来了还了3D眼镜,线程7又在同一时间拿着3D眼镜进去看电影了,后续线程都是如此执行的,每次只是进入两个线程。
简单的使用看到了,我们来看看底层的源码设计吧。开始的时候我们是创建一个Semaphore内部票据数目给予的是2。
//1.创建初始票据是2的Semaphore
Semaphore semaphore = new Semaphore(2);
//2.进入Semaphore,查看数据2是如何存储的.
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
//3.底层还是基于sync 创建了一个对象,但不同于过去ReetrantLock的是,这次是一个非公平的锁对象,我们再次进入NonfairSync看看那个数字2到底放在哪里了.
Sync(int permits) {
setState(permits);
}
//4.我们可以看到底层还是用State来存储的.
这次没有把所有代码全部粘出来,感觉那样像是凑篇幅一样。
通过上述代码,我们可以看到,我们的初始票据数,是上一次那个state来存的。

后续我们调用了acquire方法来尝试获取票据,acquire方法也可以传入获取票据数目的比如semaphore.acquire(2);也是可以的。我们进入acquire方法来看看到底是如何获取的。
//从new Semaphore(2);点击进入后续方法
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//我们可以看到,当我们没有传需要获取多少票据的时候,会默认给予1这个参数,我们来继续看后续流程
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//Thread.interrupted()判断当前线程是否已经中断,如果中断我直接抛出异常,电影都演完了,我拿3D眼镜还有毛线用.
//tryAcquireShared(arg)尝试获取票据,arg是1,刚才给予的默认1
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
//内部有实现关系,所以调用的是Semaphore类nonfairTryAcquireShared方法,我们来解读一下
//直接就是一个死循环, int available = getState();获取一下当前还有多少票据
// int remaining = available - acquires;计算出当前票据减去所需票据的一个剩余值
//if (remaining < 0 || compareAndSetState(available, remaining))我们现有2个票据,拿走1个,剩余1个,所以remaining < 0 一定是false的
//再来看另一半compareAndSetState,用原子计算(上次博客说过为什么要原子计算)方式来修改剩余票据,这个是可以修改成功的.所以满足条件可以返回一个2-1 也就是返回一个正数1
是不是有点看懵圈了,很多小伙伴感觉if (remaining < 0 ||compareAndSetState(available, remaining))前面的remaining<0,这个或判断貌似没用啊,来张图解释一下。

有没有感觉好点了,自己可以跟着源代码走一走,获取的过程就差一个doAcquireSharedInterruptibly还没有看了,如果获取超过了票据数,也就是不应该让返回负数时运行doAcquireSharedInterruptibly方法,我们来看一下。
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) {//>= 0表示获取票据成功
setHeadAndPropagate(node, r);//更改头节点
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //剔除不可用的Node节点
parkAndCheckInterrupt()) //阻塞当前线程
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
经过两次以上的尝试,我们将该线程阻塞了,不至于一直for循环在运行,也就这样,票据发放完毕了。

过程差不多就是这样的,我们可以再仔细看一下是如何添加节点的,上次ReetrantLock说了一些,我们这次再来看一下。我们现已第一次塞节点为例,
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {//第一次一定是空的,我们现在已初始塞节点为例。
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//为空直接进入这个逻辑
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize //1.第一次一定是空 //二次循环不为空 进入else
if (compareAndSetHead(new Node()))//2.创建一个空节点,并且作为head节点.
tail = head;//3.tail指向那个head节点
} else {
node.prev = t;//4. 将node节点的前驱指针指向
if (compareAndSetTail(t, node)) {//5.原子计算方式将node节点后驱节点指向tail
t.next = node;//6.将t节点(空节点)的后驱指针指向node节点
return t;
}
}
}
}

第一次循环只是一个内部的初始空节点,第二次循环才是移动指针塞入的过程。

节点唤醒是在释放票据时被唤醒的,代码超级简单,可以自己当做一份作业,自己去看一遍代码吧~!提示流程就是先还票据,然后唤醒。Semaphore差不多就这些知识点,我也带着大家简单的看了一遍源码。我们再来继续看一下后面AQS的一些工具类。
CountDownLaunch的基本使用
CountDownLaunch很好理解,也是比较实用的,我们干王者农药的时候就是一个很好的栗子,游戏选完人物大家一起加载地图等游戏资料,有的人慢,有的人快,这时就印出来了CountDownLaunch,相当于我们5个玩家同时开启5个线程,然后一起执行,执行完毕先等着,直到5个玩家全部执行完成时,才可以运行后续操作。我们来看一下代码。
public class CountDownLaunchSample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(new playerOne(countDownLatch)).start();
new Thread(new playerTwo(countDownLatch)).start();
countDownLatch.await();
System.out.println("全部加载完成");
}
static class playerOne implements Runnable {
CountDownLatch countDownLatch;
public playerOne(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public void run() {
try {
System.out.println("玩家1开始加载...");
Thread.sleep(2000);
System.out.println("玩家1加载完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (countDownLatch != null)
countDownLatch.countDown();
}
}
}
static class playerTwo implements Runnable {
CountDownLatch countDownLatch;
public playerTwo(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public void run() {
try {
System.out.println("玩家2开始加载...");
Thread.sleep(10000);
System.out.println("玩家2加载完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (countDownLatch != null)
countDownLatch.countDown();
}
}
}
}

实际项目中如果遇到读取excel多个sheet页签然后汇总数据的情况也可以采用CountDownLanch。注意最后final的countDownLatch.countDown()方法,也是一个类似上面票据增减的方法。
CyclicBarrier栅栏的简单使用:
CyclicBarrier和我们上面的CountDownLanch差不多,都是开启多个任务一起去执行,不同的是CountDownLanch需要支线任务执行完成然后CountDownLanch做一个汇总,然后继续运行后续程序。CyclicBarrier不需要做汇总。再就是CyclicBarrier是可以重复的。
public class CyclicBarrierTest implements Runnable {
private CyclicBarrier cyclicBarrier;
private int index ;
public CyclicBarrierTest(CyclicBarrier cyclicBarrier, int index) {
this.cyclicBarrier = cyclicBarrier;
this.index = index;
}
public void run() {
try {
System.out.println("index: " + index);
index--;
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(11, new Runnable() {
public void run() {
System.out.println("所有特工到达屏障,准备开始执行秘密任务");
}
});
for (int i = 0; i < 10; i++) {
new Thread(new CyclicBarrierTest(cyclicBarrier, i)).start();
}
cyclicBarrier.await();
System.out.println("全部到达屏障....");
}
}
这个需要注意的是CyclicBarrier cyclicBarrier = new CyclicBarrier(11, 这个11,就是说一定有11个线程执行完毕,我才可以执行后面的操作,我们下面for循环是10,而我们那里写的是11啊,别忘记还有一个主线程呢,所以说每次计算一定加一个主线程啊。
Exchanger的简单使用
最后就是我们Exchanger,平时使用的不多,我们了解一下就可以了,搂一眼代码,就是线程之间的变量交换。
public static void main(String []args) {
final Exchanger<Integer> exchanger = new Exchanger<Integer>();
for(int i = 0 ; i < 4 ; i++) {
final Integer num = i;
new Thread() {
public void run() {
System.out.println("我是线程:Thread_" + this.getName() + "我的数据是:" + num);
try {
Integer exchangeNum = exchanger.exchange(num);
Thread.sleep(1000);
System.out.println("我是线程:Thread_" + this.getName() + "我原先的数据为:" + num + " , 交换后的数据为:" + exchangeNum);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
总结:
这次我们核心梳理了我们的Semaphore的执行流程,内部是如何来实现我们的票据计数,获取,归还等操作的,再就是我们for无限循环会在两次以后自动阻塞的设计思想,还有我们的CountDownLanch、CyclicBarrier、Executors的基本使用,并赋予大家简单的代码流程,今天就说到这,明天我们继续来说我们的多线程。


java架构之路(多线程)JUC并发编程之Semaphore信号量、CountDownLatch、CyclicBarrier栅栏、Executors线程池的更多相关文章
- 多线程进阶——JUC并发编程之CountDownLatch源码一探究竟
1.学习切入点 JDK的并发包中提供了几个非常有用的并发工具类. CountDownLatch. CyclicBarrier和 Semaphore工具类提供了一种并发流程控制的手段.本文将介绍Coun ...
- 并发编程之 Semaphore 源码分析
前言 并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore--信号量,关于他的使用请查看往期文章并发编程之 线程 ...
- java并发编程之Semaphore
信号量(Semaphore).有时被称为信号灯.是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们可以正确.合理的使用公共资源. 一个计数信号量.从概念上讲,信号量维护了一个许可集.如 ...
- JUC 并发编程--09, 阻塞队列: DelayQueue, PriorityBlockingQueue ,SynchronousQueue, 定时任务线程池: ScheduledThreadPoolExecutor
先看DelayQueue 这个是用优先级队列实现的无界限的延迟队列,直接上代码: /** * 这个是 {@link DelayQueue} 延时队列 的验证使用类 */ class MyDelayed ...
- Java并发编程之CAS第一篇-什么是CAS
Java并发编程之CAS第一篇-什么是CAS 通过前面几篇的学习,我们对并发编程两个高频知识点了解了其中的一个—volatitl.从这一篇文章开始,我们将要学习另一个知识点—CAS.本篇是<凯哥 ...
- Java并发编程之CAS第三篇-CAS的缺点及解决办法
Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- [转帖]java架构之路-(面试篇)JVM虚拟机面试大全
java架构之路-(面试篇)JVM虚拟机面试大全 https://www.cnblogs.com/cxiaocai/p/11634918.html 下文连接比较多啊,都是我过整理的博客,很多答案都 ...
- Java并发编程之set集合的线程安全类你知道吗
Java并发编程之-set集合的线程安全类 Java中set集合怎么保证线程安全,这种方式你知道吗? 在Java中set集合是 本篇是<凯哥(凯哥Java:kagejava)并发编程学习> ...
随机推荐
- java List接口中常用类
Vector:线程安全,但速度慢,已被ArrayList替代. ArrayList:线程不安全,查询速度快. LinkedList:链表结构,增删速度快.取出List集合中元素的方式: get(int ...
- H5 存储数据sessionStorage
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- vue 项目使用局域网多端访问并实时自动更新(利用 browser-sync)
在写vue的项目中时,虽然vue会自动更新页面,但是切换页面切来切去也很麻烦,有时候我们还要在公司另一台电脑或者手机上调试,这时候利用browser-sync插件,无需改动vue的代码即可实现: 1. ...
- 随机生成验证码(JS)
效果展示 实现原理 1. html:一般就是一个div: <div id="code"></div> ,样式根据需求设计. 2. JS:1)将所有的验证码所 ...
- WNMP nginx+php5+mysql测试环境安装(Windows7)(二)
3. 安装Zend Optimizer Zend Optimizer对那些在被最终执行之前由Run-Time Complier产生的代码进行优化,提高PHP应用程序的执行速度.一般情况下,执行使用Ze ...
- CachedRowSet 接口
Sun Microsystems 提供的 CachedRowSet 接口的参考实现是一个标准实现.开发人员可以按原样使用此实现.可以扩展它,也可以选择自己编写此接口的实现. CachedRowSet ...
- freemarker<一>
FreeMarker是一个模板引擎,一个基于模板生成文本输出的通用工具,使用纯Java编写.FreeMarker被设计用来生成HTMLWeb页面,特别是基于MVC模式的应用程序. 所谓模板,就是一份已 ...
- Python - Tuple 怎么用,为什么有 tuple 这种设计?
背景 看到有同学很执着的用 tuple,想起自己刚学 python 时,也是很喜欢 tuple,为啥?因为以前从来没见过这种样子的数据 (1,2), 感觉很特别,用起来也挺好用 i,j=(1,2), ...
- git之github解决冲突
1.先创建一个txt文件,并进行编辑 2.然后推送到github,过程看之前的教程. 3.在另一个文件夹拉取(用小乌龟拉取),分别在克隆文件夹和原本文件夹操作test.txt. 4.把本体推送给服务器 ...
- centos7搭建Fabric基础环境
一.首先升级centos最新内核 参考https://www.cnblogs.com/sky-cheng/p/12146054.html 二.卸载旧版本docker [root@localhost ~ ...