前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:
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开发笔记(一百零二)信号量的请求与释放的更多相关文章

  1. Java开发笔记(九十二)文件通道的基本用法

    前面介绍的各色流式IO在功能方面着实强大,处理文件的时候该具备的操作应有尽有,可流式IO在性能方面不尽如人意,它的设计原理使得实际运行效率偏低,为此从Java4开始增加了NIO技术,通过全新的架构体系 ...

  2. Java开发笔记(十二)布尔变量论道与或非

    在编程语言的设计之初,它们除了可以进行数学计算,还常常用于逻辑推理和条件判断.为了实现逻辑判断的功能,Java引入了一种布尔类型boolean,用来表示“真”和“假”.该类型的变量只允许两个取值,即t ...

  3. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  4. Java开发笔记(一百零三)线程间的通信方式

    前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...

  5. Java开发笔记(一百零一)通过加解锁避免资源冲突

    前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...

  6. Java开发笔记(一百二十五)AWT图像加工

    前面介绍了如何使用画笔工具Graphics绘制各种图案,然而Graphics并不完美,它的遗憾之处包括但不限于:1.不能设置背景颜色:2.虽然提供了平移功能,却未提供旋转功能与缩放功能:3.只能在控件 ...

  7. Java开发笔记(一百零四)普通线程池的运用

    前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...

  8. Java开发笔记(一百零五)几种定时器线程池

    前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...

  9. Java开发笔记(一百零六)Fork+Join框架实现分而治之

    前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...

随机推荐

  1. Mac中文乱码问题

    在终端切换到文档所在的目录,输入下面的命令: iconv -c -f GB2312 -t UTF-8 乱码的文件名 >> 新文件的名称

  2. python模块之shutil和zipfile

    shutil 模块 高级的 文件.文件夹.压缩包 处理模块 shutil.copyfileobj(fsrc, fdst[, length])将文件内容拷贝到另一个文件中 import shutil s ...

  3. TCP的三次握手和四次握手

    三次握手(建立连接) 首先,服务器进程(B)先创建传控制块TCB(用来存储连接信息,如连接表,发送和接收序号等),准备接收客户进程(A)的请求.然后服务器进程处于LISTEN(收听)状态,等待客户的连 ...

  4. Linux学习-SELinux 初探

    什么是 SELinux 什么是 SELinux 呢?其实他是『 Security Enhanced Linux 』的缩写,字面上的意义就是安全强化的 Linux 之意! 当初设计的目标:避免资源的误用 ...

  5. hdu4489 组合公式+dp

    这里对于题意在说明一下, 题目中要求的排列必须是波浪形,每一个在排列中的人不是波峰就是波谷,如果它既不是波峰也不是波谷排列就是错的. 对于我这种数学渣渣来说,做一道dp题要好久,%>_<% ...

  6. JAVA 基础--开发环境IDEA 搭建

    1.下载IDEA  (500M+) 2.激活. 在网站http://idea.lanyus.com/中获取注册码,填入Activation code中: 然后点击Activate即可. 3.创建工程前 ...

  7. 和为n连续正数序列 【微软面试100题 第五十一题】

    题目要求: 输入一个正数n,输出所有和为n连续正数序列(至少两个). 例如输入15,由于1+2+3+4+5 = 4+5+6 = 7+8 = 15.所以输出3个连续序列1~5,4~6,7~8. 参考资料 ...

  8. Java 程序动态替换 docx 模板中定制值的实现例子

    项目系统中打印功能,导出 word 文档功能是挺常用的,本文介绍自定文档模板,程序实现模板内容中值替代的功能. 模板文件 template.docx ​ 执行 main public static v ...

  9. [python][oldboy] * **的用法

    * 和**主要用在函数的参数中, # coding=utf8 """ 三种编码: 1 python程序代码的编码 # coding=utf8 2 设置/查看python程 ...

  10. adb pull 文件夹的时候注意

    传说之美 分享快乐 记录生活 学习探索 博客园 首页 新随笔 联系 管理 订阅 随笔- 75  文章- 0  评论- 19  Android 用adb pull或push 拷贝手机文件到到电脑上,拷贝 ...