前面介紹了同步與加鎖兩種併發處理機制,雖然加鎖比起同步要靈活一些,可是加鎖在某些高級場合依然力有未逮,包括但不限於下列幾點:
一、某塊代碼被加鎖以後,對其它線程而言就處於繁忙狀態,缺少彈性的閾值範圍;
二、遇到被其它線程加鎖的狀況,當前線程要麼一直等待,要麼當即放棄,除了這兩種反應以外,沒有別的選擇了;
三、線程A加鎖以後,只能由線程A解鎖,要是線程A忘了解鎖,那麼被鎖住的資源將沒法釋放,從而致使其它線程出現死鎖的狀況;
有鑑於此,Java又設計了一種信號量工具Semaphore,試圖從根本上解決加鎖機制的不足之處。所謂信號量關鍵在於數量的量,它裏面保存的是許可證,而且許可證的數量還不止一個,這意味着有幾個許可證,就容許幾個線程一塊兒處理。好比某個停車場有五個停車位,每輛汽車停進來都會佔據一個停車位;相對應的,停車場每開出一輛汽車,都會釋放一個停車位,空出來的停車位能夠留給下一輛汽車停泊。把停車業務抽象爲信號量機制,至關於某個信號量擁有五個許可證,每一個停車線程在處理過程當中都會佔據一個許可證,那麼該信號量便容許五個停車線程同時進行處理,此時再來第六個線程的話才須要在旁邊等待,直到五個停車線程的其中之一釋放本身佔據的許可證以後,第六個線程再得到空出來的許可證並往下處理。
信號量還支持多種請求許可證的方式,用以知足豐富多樣的業務需求,常見的許可證請求方式主要有如下四種:
一、堅持請求從信號量中得到許可證,即便收到線程中斷信號也不放棄;若是信號量無空閒許可證,那麼願意繼續等待直到得到許可證。該方式調用的是信號量的acquireUninterruptibly方法。
二、嘗試從信號量中得到許可證,但只願意等待有限的時間;要是等待時長超過規定時間,那就再也不等待,放棄得到許可證。該方式調用的是信號量的tryAcquire方法(注意是帶時間參數的同名方法),該方法返回true表示在等待期間得到了許可證,返回false表示因超時放棄了等待。
三、嘗試從信號量中當即得到許可證,哪怕一丁點時間都不肯意等待。該方式調用的是信號量的tryAcquire方法(注意是不帶參數的同名方法),該方法返回true表示獲得了許可證,返回false表示沒獲得許可證。
四、請求從信號量中得到許可證,若是信號量無空閒許可證,那麼願意繼續等待,但在等待期間容許接收中斷信號。該方式調用的是信號量的acquire方法。
除此以外,信號量提供了release方法用來釋放信號量資源,每調用一次release方法便釋放一個許可證,並且釋放的許可證既多是當前線程請求的,也多是其它線程請求的,這就避免了死鎖現象的發生。
接下來舉個實際應用的例子,每逢一年一度的春運來臨之際,想回家過年的人們紛紛涌向火車站買票,不一樣的旅客有着不同的耐心。有的旅客頗有耐心地排隊,必定要買到車票纔會離開,即便颳風下雨也不放棄;有的旅客有一些耐心,願意在買票隊伍中等上一時半刻,可是不想等過久,一旦等待時間超過忍耐限度,就放棄排隊另想辦法;有的旅客很是着急,要求當即立刻買到車票,一下子都等來不及,只要前面有人排隊那就轉身離開去訂飛機票了;還有的旅客也願意排隊,但他一邊排隊一邊拿起手機約順風車,假若在排隊期間成功約上了順風車,那便跑去坐順風車回家了。
按照上面的買票需求,區分四種買票方式的業務邏輯,可編寫以下所示的買票任務代碼:html
//定義一個買票的任務 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 必定要買到車票的旅客 買到票啦
從買票日誌可見,須要當即買到票的旅客幾乎都買不到車票,一邊排隊一邊約順風車的旅客也有必定機率買不到票,而願意排一下子隊的旅客和必定要買到車票的旅客則一般都能買到車票。dom
更多Java技術文章參見《Java開發筆記(序)章節目錄》ide