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开发笔记(序)章节目录》