Java开发笔记(一百零二)信号量的请求与释放
前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:
1、某块代码被加锁之后,对其它线程而言就处于繁忙状态,缺乏弹性的阈值范围;
2、遇到被其它线程加锁的情况,当前线程要么一直等待,要么立即放弃,除了这两种反应之外,没有别的选择了;
3、线程A加锁之后,只能由线程A解锁,要是线程A忘了解锁,那么被锁住的资源将无法释放,从而导致其它线程出现死锁的情况;
有鉴于此,Java又设计了一种信号量工具Semaphore,试图从根本上解决加锁机制的不足之处。所谓信号量关键在于数量的量,它里面保存的是许可证,并且许可证的数量还不止一个,这意味着有几个许可证,就允许几个线程一起处理。比如某个停车场有五个停车位,每辆汽车停进来都会占据一个停车位;相对应的,停车场每开出一辆汽车,都会释放一个停车位,空出来的停车位可以留给下一辆汽车停泊。把停车业务抽象为信号量机制,相当于某个信号量拥有五个许可证,每个停车线程在处理过程中都会占据一个许可证,那么该信号量便允许五个停车线程同时进行处理,此时再来第六个线程的话才需要在旁边等待,直到五个停车线程的其中之一释放自己占据的许可证之后,第六个线程再获得空出来的许可证并往下处理。
信号量还支持多种请求许可证的方式,用以满足丰富多样的业务需求,常见的许可证请求方式主要有以下四种:
1、坚持请求从信号量中获得许可证,即使收到线程中断信号也不放弃;如果信号量无空闲许可证,那么愿意继续等待直到获得许可证。该方式调用的是信号量的acquireUninterruptibly方法。
2、尝试从信号量中获得许可证,但只愿意等待有限的时间;要是等待时长超过规定时间,那就不再等待,放弃获得许可证。该方式调用的是信号量的tryAcquire方法(注意是带时间参数的同名方法),该方法返回true表示在等待期间获得了许可证,返回false表示因超时放弃了等待。
3、尝试从信号量中立即获得许可证,哪怕一丁点时间都不愿意等待。该方式调用的是信号量的tryAcquire方法(注意是不带参数的同名方法),该方法返回true表示得到了许可证,返回false表示没得到许可证。
4、请求从信号量中获得许可证,如果信号量无空闲许可证,那么愿意继续等待,但在等待期间允许接收中断信号。该方式调用的是信号量的acquire方法。
除此之外,信号量提供了release方法用来释放信号量资源,每调用一次release方法便释放一个许可证,而且释放的许可证既可能是当前线程请求的,也可能是其它线程请求的,这就避免了死锁现象的发生。
接下来举个实际应用的例子,每逢一年一度的春运来临之际,想回家过年的人们纷纷涌向火车站买票,不同的旅客有着不一样的耐心。有的旅客很有耐心地排队,一定要买到车票才会离开,即使刮风下雨也不放弃;有的旅客有一些耐心,愿意在买票队伍中等上一时半刻,但是不想等太久,一旦等待时间超过忍耐限度,就放弃排队另想办法;有的旅客非常着急,要求立即马上买到车票,一会儿都等来不及,只要前面有人排队那就转身离开去订飞机票了;还有的旅客也愿意排队,但他一边排队一边拿起手机约顺风车,倘若在排队期间成功约上了顺风车,那便跑去坐顺风车回家了。
按照上面的买票需求,区分四种买票方式的业务逻辑,可编写如下所示的买票任务代码:
//定义一个买票的任务
public class BuyTicket implements Runnable {
public final static int FULL_PAITIENCE = 1; // 极有耐心
public final static int SOME_PAITIENCE = 2; // 有些耐心
public final static int LACK_PAITIENCE = 3; // 缺少耐心
public final static int ACCEPT_INTERRUPT = 4; // 接受中断
private Semaphore semaphore; // 信号量
private int person_type; // 用户类型 public BuyTicket(Semaphore semaphore, int person_type) {
this.semaphore = semaphore;
this.person_type = person_type;
} @Override
public void run() {
if (person_type == FULL_PAITIENCE) { // 极有耐心的旅客
// 请求从信号量中获得许可证,并且不接受中断。
// 如果信号量无空闲许可证,那么愿意继续等待直到获得许可证。
semaphore.acquireUninterruptibly();
wait_a_moment(); // 稍等一会儿
PrintUtils.print(Thread.currentThread().getName(), "买到票啦");
semaphore.release(); // 释放信号量资源
} else if (person_type == SOME_PAITIENCE) { // 有些耐心的旅客
try {
// 尝试从信号量中获得许可证,但只愿意等待80毫秒。
// 如果在规定时间内获得许可证就返回true,如果未获得许可证就返回false。
boolean result = semaphore.tryAcquire(80, TimeUnit.MILLISECONDS);
if (result) { // 已获得许可证
wait_a_moment(); // 稍等一会儿
PrintUtils.print(Thread.currentThread().getName(), "买到票啦");
} else { // 未获得许可证
PrintUtils.print(Thread.currentThread().getName(), "等太久,不买票了");
}
} catch (InterruptedException e) { // 等待期间接受中断
e.printStackTrace();
} finally {
semaphore.release(); // 释放信号量资源
}
} else if (person_type == LACK_PAITIENCE) { // 缺少耐心的旅客
// 尝试从信号量中立即获得许可证,哪怕1毫秒都不愿意等待。
// 获得许可证就返回true,未获得许可证就返回false。
boolean result = semaphore.tryAcquire();
if (result) { // 已获得许可证
wait_a_moment(); // 稍等一会儿
PrintUtils.print(Thread.currentThread().getName(), "买到票啦");
} else { // 未获得许可证
PrintUtils.print(Thread.currentThread().getName(), "一会都不想等,不买票了");
}
semaphore.release(); // 释放信号量资源
} else if (person_type == ACCEPT_INTERRUPT) { // 接受中断的旅客。一边排队一边约顺风车
try {
// 请求从信号量中获得许可证,并且接受中断。
// 如果信号量无空闲许可证,那么愿意继续等待,但收到中断信号除外。
semaphore.acquire();
wait_a_moment(); // 稍等一会儿
PrintUtils.print(Thread.currentThread().getName(), "买到票啦");
} catch (InterruptedException e) { // 收到了顺风车接单的通知
PrintUtils.print(Thread.currentThread().getName(), "约到顺风车,不买票了");
} finally {
semaphore.release(); // 释放信号量资源
}
}
} // 稍等一会儿,模拟窗口买票的时间消耗
public static void wait_a_moment() {
int delay = new Random().nextInt(100); // 生成100以内的随机整数
try {
Thread.sleep(delay); // 睡眠若干毫秒
} catch (InterruptedException e2) {
}
}
}
然后在主线程分别启动若干个买票线程,假设当前开了三个售票窗口,四类旅客各来五位买票,陆陆续续总共有二十位旅客前来排队。那么演示众人买票的测试代码示例如下:
// 测试许多旅客一起买票的场景
private static void testManyTask() {
// 创建拥有三个许可证的信号量
Semaphore semaphore = new Semaphore(3);
// 一定要买到车票
BuyTicket alwaysBuy = new BuyTicket(semaphore, BuyTicket.FULL_PAITIENCE);
// 为了买到车票愿意排队一会儿,但要是等太久,就放弃买票
BuyTicket awhileBuy = new BuyTicket(semaphore, BuyTicket.SOME_PAITIENCE);
// 需要立即买到票,否则马上离开
BuyTicket immediateBuy = new BuyTicket(semaphore, BuyTicket.LACK_PAITIENCE);
// 先排队看看,如果有其它途径可以回家,就不用买票了
BuyTicket caseBuy = new BuyTicket(semaphore, BuyTicket.ACCEPT_INTERRUPT);
// 创建接受中断的排队买票线程数组
Thread[] caseThread = new Thread[5];
for (int i=0; i<20; i++) { // 下面依次创建并启动20个买票线程
if (i%4 == 0) { // 这些旅客一定要买到车票
new Thread(alwaysBuy, "一定要买到车票的旅客").start(); // 启动买票线程A
} else if (i%4 == 1) { // 这些旅客愿意排一会儿队
new Thread(awhileBuy, "愿意排一会儿队的旅客").start(); // 启动买票线程B
} else if (i%4 == 2) { // 这些旅客需要立即买到票
new Thread(immediateBuy, "需要立即买到票的旅客").start(); // 启动买票线程C
} else if (i%4 == 3) { // 这些旅客一边排队一边约顺风车
// 创建一个接受中断的排队买票线程
caseThread[i/4] = new Thread(caseBuy, "一边排队一边约顺风车的旅客");
caseThread[i/4].start(); // 启动买票线程D
}
}
BuyTicket.wait_a_moment(); // 稍等一会儿
// 给一边排队一边约顺风车的买票线程们发送中断信号
for (Thread thread : caseThread) {
thread.interrupt(); // 发送中断通知,比如顺风车接单了等等
}
}
运行以上的买票测试代码,观察到以下的买票日志:
12:04:41.458 需要立即买到票的旅客 一会都不想等,不买票了
12:04:41.458 一定要买到车票的旅客 买到票啦
12:04:41.458 需要立即买到票的旅客 一会都不想等,不买票了
12:04:41.458 需要立即买到票的旅客 一会都不想等,不买票了
12:04:41.458 需要立即买到票的旅客 一会都不想等,不买票了
12:04:41.462 愿意排一会儿队的旅客 买到票啦
12:04:41.462 愿意排一会儿队的旅客 买到票啦
12:04:41.471 一边排队一边约顺风车的旅客 买到票啦
12:04:41.471 一边排队一边约顺风车的旅客 约到顺风车,不买票了
12:04:41.471 一边排队一边约顺风车的旅客 买到票啦
12:04:41.472 一边排队一边约顺风车的旅客 约到顺风车,不买票了
12:04:41.472 一边排队一边约顺风车的旅客 约到顺风车,不买票了
12:04:41.474 需要立即买到票的旅客 买到票啦
12:04:41.491 愿意排一会儿队的旅客 买到票啦
12:04:41.498 愿意排一会儿队的旅客 买到票啦
12:04:41.537 一定要买到车票的旅客 买到票啦
12:04:41.552 一定要买到车票的旅客 买到票啦
12:04:41.558 一定要买到车票的旅客 买到票啦
12:04:41.563 愿意排一会儿队的旅客 买到票啦
12:04:41.566 一定要买到车票的旅客 买到票啦
从买票日志可见,需要立即买到票的旅客几乎都买不到车票,一边排队一边约顺风车的旅客也有一定概率买不到票,而愿意排一会儿队的旅客和一定要买到车票的旅客则通常都能买到车票。
更多Java技术文章参见《Java开发笔记(序)章节目录》
Java开发笔记(一百零二)信号量的请求与释放的更多相关文章
- Java开发笔记(九十二)文件通道的基本用法
前面介绍的各色流式IO在功能方面着实强大,处理文件的时候该具备的操作应有尽有,可流式IO在性能方面不尽如人意,它的设计原理使得实际运行效率偏低,为此从Java4开始增加了NIO技术,通过全新的架构体系 ...
- Java开发笔记(十二)布尔变量论道与或非
在编程语言的设计之初,它们除了可以进行数学计算,还常常用于逻辑推理和条件判断.为了实现逻辑判断的功能,Java引入了一种布尔类型boolean,用来表示“真”和“假”.该类型的变量只允许两个取值,即t ...
- Java开发笔记(序)章节目录
现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...
- Java开发笔记(一百零三)线程间的通信方式
前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...
- Java开发笔记(一百零一)通过加解锁避免资源冲突
前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...
- Java开发笔记(一百二十五)AWT图像加工
前面介绍了如何使用画笔工具Graphics绘制各种图案,然而Graphics并不完美,它的遗憾之处包括但不限于:1.不能设置背景颜色:2.虽然提供了平移功能,却未提供旋转功能与缩放功能:3.只能在控件 ...
- Java开发笔记(一百零四)普通线程池的运用
前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...
- Java开发笔记(一百零五)几种定时器线程池
前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...
- Java开发笔记(一百零六)Fork+Join框架实现分而治之
前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...
随机推荐
- python数据类型之字符串(str)和其常用方法
字符串是有序的,不可变的. 下面的例子说明了字符串是不可变的 name = 'alex' name = 'Jack' """ 并没有变,只是给name开启了一块新内存,储 ...
- debian7不能apt安装emacs24
维护者在主页上 http://emacs.naquadah.org/ 提到: These packages are not maintained anymore I don't use these p ...
- jenkins配置邮箱时出错
jenkins配置邮箱时出错: 这有可能是此博客http://www.cnblogs.com/yajing-zh/p/5109517.html在配置jenkins发送邮件时的第4步和第5步中的邮箱不匹 ...
- hdu 1011 Starship Troopers(树形背包)
Starship Troopers Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Other ...
- powerdesigner约束名唯一出错的解决办法
powerdesigner中自动生成的约束名有时会因为表的前缀一样而不具有唯一性,这样在生成时就会出错,一般的解决办法有以下两种: 1.模型=>Reference中可以看到当前模型中的所有Ref ...
- day01_08.三大控制结构
编程三要素:变量,运算,控制 控制: 有选择性的控制让你某部分代码执行,某部分不执行,或者来回反复执行某段代码 控制的三种基本机构:顺序,选择,循环 1.顺序 程序从上到下,顺序执行 <?php ...
- [办公软件篇][3]windows软件安装
http://www.jeffjade.com/2015/10/19/2015-10-18-Efficacious-win-software/
- PHPSTORM杂技
PHPSTORM技巧 去掉右上角弹出浏览器条 settings->web browsers->show browser popup in the editor前的钩去掉 让class fu ...
- 【SCOI2003】【BZOJ1092】蜘蛛难题
有一堆管道,还有一个蜘蛛Willy,如下图所示.所有管道的是上端开口,下端封底,直径都是1cm,连接两个管道的连接容量无限,但体积可以忽略不计. 在第一个管道上方有一个水源,从中有水不断往下流,速度为 ...
- 微信小程序红包开发思路 微信红包小程序开发思路讲解
之前公司开发小程序红包,将自己在开发的过程中遇到的一些坑分享到了博客里.不少人看了以后,还是不明白怎么开发.也加了我微信咨询.所以今天,我就特意再写一篇文章,这次就不谈我开发中遇到的坑了.就主要给大家 ...