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模块之pickle
和json不同的是: json只支持str,int,tuple,list,dict. pickle支持python里所有的数据类型,但是只能在python里序列化,不跨平台,python独有. 代码示 ...
- 原来针对新唐mcu,keil有免费许可
MDK for Nuvoton Cortex-M0/M23:The MDK for Nuvoton Cortex-M0/M23 is a license paid by Nuvoton. It is ...
- CentOS 7.X 中systemctl命令用法详解
systemctl是RHEL 7 的服务管理工具中主要的工具,它融合之前service和chkconfig的功能于一体.可以使用它永久性或只在当前会话中启用/禁用服务,下面来看CentOS 7.X 中 ...
- selenium2基本控件介绍及其代码
输入框:input 表现形式: 1.在html中一般为:<input id="user" type="text"> 主要操作: ...
- CodeForces 500E New Year Domino
题意: 从左到右排列着\(n\)个多米诺骨牌,它们分别站在\(x\)轴上的位置\(p_i\)上且高度为\(l_i\). 当第\(i\)个多米诺骨牌向右倒下时,如果\(p_i < p_j \leq ...
- javascript常见数据集
目录 数组 对象 一.数组 创建方法 1 2 3 var arrayObj = new Array(); //创建一个数组 var arrayObj = new Array([ ...
- IO Streams:字符流
简介 Java平台使用Unicode约定存储字符值.字符流I / O自动将此内部格式转换为本地字符集.在西方,本地字符集通常是ASCII的8位超集. 对于大多数应用,具有字符流的I / O并不比具有字 ...
- asp.net的Context.Cache缓存过期策略
最近使用Context.Cache需要了解Cache的缓存过期策略. 文章:ASP.NET缓存中Cache过期的三种策略
- C# 条件与&&与条件或||的使用总结
CSDN说明: 条件“或”运算符 (||) 执行 bool 操作数的逻辑“或”运算,但仅在必要时才计算第二个操作数. 件“与”运算符 (&&) 执行其 bool 操作数的逻辑“与”运算 ...
- poj 1523 SPF 求割点以及删除该割点后联通块的数量
SPF Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 7136 Accepted: 3255 Description C ...