Java 并發(fā)工具之 Semaphore
1. 前言
從本節(jié)開始,我們學(xué)習(xí)新一章內(nèi)容 —— 同步工具。
本節(jié)帶領(lǐng)大家認(rèn)識(shí)第一個(gè)常用的 Java 并發(fā)工具類之 Semaphore。
本節(jié)先通過一個(gè)生活中的例子為大家通俗解釋一下什么是 Semaphore 信號(hào)量,接著介紹 Semaphore 工具類的最基本用法,有了這些基本認(rèn)識(shí)之后,給出 Semaphore 工具最常用的場(chǎng)合說明,然后通過簡(jiǎn)單的編碼實(shí)現(xiàn)文中提到的生活案例,讓大家有一個(gè)理性的認(rèn)識(shí),之后帶領(lǐng)大家熟悉 Semaphore 最常用的一些編程方法,最后通過同類工具的比較,進(jìn)一步加深對(duì) Semaphore 工具類的理解。
Semaphore 工具類本身使用很簡(jiǎn)單,重點(diǎn)是對(duì)常用編程方法的準(zhǔn)確理解。
當(dāng)我們遇到各類需要做并發(fā)控制的場(chǎng)合時(shí),怎么做到選取最合適的并發(fā)工具加以應(yīng)用呢?唯有多加練習(xí),不斷總結(jié)各種并發(fā)工具之間的區(qū)別,透徹理解各類工具的應(yīng)用場(chǎng)合,才能做到游刃有余,手到擒來。
下面我們正式開始介紹吧。
2. 概念解釋
從 JDK1.5 開始提供,Java 官方就在 java.util.concurrent 并發(fā)包中提供了 Semaphore 工具類。
那什么是 “Semaphore” 呢?單詞 “Semaphore” 在計(jì)算機(jī)世界中被解釋為中文 “信號(hào)量” ,但更能表述其含義的叫法應(yīng)該是 “許可證管理器”。不管叫什么中文名稱,它就是一種計(jì)數(shù)信號(hào)量,用于管理一組資源,給資源的使用者規(guī)定一個(gè)量從而控制同一時(shí)刻的使用者數(shù)目。
這樣的解釋是不是很抽象?沒關(guān)系,在此為大家舉一個(gè)生活中通俗的例子,讓大家先對(duì) “信號(hào)量” 及其應(yīng)用有一個(gè)感性的認(rèn)識(shí)。
大家先觀察一下下面過閘機(jī)的圖例,回想一下我們平時(shí)過閘機(jī)的場(chǎng)景。
(圖片來自網(wǎng)絡(luò),圖片版權(quán)歸作者所有)
比如上圖中過閘機(jī)就是信號(hào)量的基本運(yùn)用。
上圖中的乘客就類比是我們程序里面的各類線程,閘機(jī)就類比是一類線程需要使用的資源,而信號(hào)量就是某一時(shí)刻可用的閘機(jī)數(shù)量。
當(dāng)某個(gè)時(shí)刻有乘客需要使用閘機(jī)過站時(shí),首先他需要找到一臺(tái)沒有人使用的閘機(jī),現(xiàn)實(shí)中他通過眼睛觀察即可知道,在我們程序里面就是需要觀察信號(hào)量,看能不能申請(qǐng)到代表可用閘機(jī)的信號(hào)量,如果能則表示有空閑閘機(jī) (資源) 可用,否則需要等待其他乘客使用完畢 (信號(hào)量釋放)后再使用。
概念我們已經(jīng)了解了,那 Semaphore 工具類最基本的用法是怎樣的呢?別急,看下面。
3. 基本用法
// 首先創(chuàng)建 Semaphore 對(duì)象
Semaphore semaphore = new Semaphore();
// 在資源操作開始之前,先獲取資源的使用許可
semaphore.acquire();
...
// 在獲取到資源后,利用資源進(jìn)行業(yè)務(wù)處理
...
// 在資源操作完畢之后,釋放資源的使用許可
semaphore.release();
...
是不是很簡(jiǎn)單,那 Semaphore 信號(hào)量在我們?nèi)粘?shí)踐中,到底應(yīng)該應(yīng)用在哪些場(chǎng)合比較合適呢?下面我們給出最常用的場(chǎng)景說明。
4. 常用場(chǎng)景
Semaphore 經(jīng)常用于限制同一時(shí)刻獲取某種資源的線程數(shù)量,最為典型的就是做流量控制。
比如 WEB 服務(wù)器處理能力有限,需要控制網(wǎng)絡(luò)請(qǐng)求接入的最大連接數(shù),以防止過大的請(qǐng)求流量壓垮我們的服務(wù)器,導(dǎo)致整個(gè)應(yīng)用不能正常提供服務(wù)。
比如數(shù)據(jù)庫(kù)服務(wù)器處理能力有限,需要控制數(shù)據(jù)庫(kù)最大連接數(shù),以防止大量某個(gè)應(yīng)用過分占有數(shù)據(jù)庫(kù)連接數(shù),導(dǎo)致數(shù)據(jù)庫(kù)服務(wù)器不能為其他的應(yīng)用提供足夠的連接請(qǐng)求。
當(dāng)在研發(fā)過程中遇到類似這些場(chǎng)景時(shí),就可以考慮直接應(yīng)用 Semaphore 工具類輔助實(shí)現(xiàn)。
上面舉的生活中過閘機(jī)的例子,如果用程序表達(dá),該如何實(shí)現(xiàn)呢?在程序中如何使用 Semaphore 信號(hào)量達(dá)到控制和應(yīng)用呢?最直接方式就是去感受最簡(jiǎn)單的例子,下面直接用最明了的代碼說明例子中如何應(yīng)用了信號(hào)量。
5. 場(chǎng)景案例
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
// 先定義一個(gè)Semaphore信號(hào)量對(duì)象
private static Semaphore semaphore = new Semaphore(3);
// 測(cè)試方法
public static void main(String[] args) {
// 定義10個(gè)人過閘機(jī)
for(int i=0; i<10; i++) {
Person person = new Person(semaphore, i);
new Thread(person).start();
}
}
}
在上面的代碼中,先創(chuàng)建了一個(gè) Semaphore 信號(hào)量對(duì)象,然后賦給了每一位進(jìn)站旅客 Person ,接下來每一位旅客如何動(dòng)作呢,看下面的代碼。
import java.util.concurrent.Semaphore;
public class Person implements Runnable {
private Semaphore semaphore;
private String persionName;
public Person(Semaphore semaphore, int persionNo) {
this.semaphore = semaphore;
this.persionName = "旅客" + persionNo;
}
public void run() {
try {
// 請(qǐng)求獲得信號(hào)量,就是請(qǐng)求(尋找)是否有可用的閘機(jī)
semaphore.acquire();
// 已經(jīng)等到了可用閘機(jī)
System.out.println(this.persionName + "已經(jīng)占有一臺(tái)閘機(jī)");
// 進(jìn)站
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 已經(jīng)進(jìn)站
System.out.println(this.persionName + "已經(jīng)進(jìn)站");
// 讓出閘機(jī)給別人用
semaphore.release();
}
}
}
在 Person 類中,首先通過 acquire 獲取了可用閘機(jī),然后休眠 2 秒代表刷卡過閘機(jī),最后在 finally 使用 release 方法讓出閘機(jī)。我們觀察一下運(yùn)行結(jié)果。
旅客0已經(jīng)占有一臺(tái)閘機(jī) <---占據(jù)了一臺(tái)
旅客1已經(jīng)占有一臺(tái)閘機(jī) <---占據(jù)了一臺(tái)
旅客6已經(jīng)占有一臺(tái)閘機(jī) <---占據(jù)了一臺(tái)
旅客0已經(jīng)進(jìn)站 <---0號(hào)旅客已經(jīng)進(jìn)站釋放了閘機(jī)
旅客3已經(jīng)占有一臺(tái)閘機(jī) <---3號(hào)旅客這個(gè)時(shí)候才拿到了可用閘機(jī)
旅客1已經(jīng)進(jìn)站 <---1號(hào)旅客已經(jīng)進(jìn)站釋放了閘機(jī)
旅客2已經(jīng)占有一臺(tái)閘機(jī) <---2號(hào)旅客這個(gè)時(shí)候才拿到了可用閘機(jī)
旅客6已經(jīng)進(jìn)站 <---6號(hào)旅客已經(jīng)進(jìn)站釋放了閘機(jī)
旅客4已經(jīng)占有一臺(tái)閘機(jī) <---4號(hào)旅客這個(gè)時(shí)候才拿到了可用閘機(jī)
旅客3已經(jīng)進(jìn)站
旅客4已經(jīng)進(jìn)站
旅客5已經(jīng)占有一臺(tái)閘機(jī)
旅客7已經(jīng)占有一臺(tái)閘機(jī)
旅客2已經(jīng)進(jìn)站
旅客8已經(jīng)占有一臺(tái)閘機(jī)
旅客7已經(jīng)進(jìn)站
旅客5已經(jīng)進(jìn)站
旅客9已經(jīng)占有一臺(tái)閘機(jī)
旅客8已經(jīng)進(jìn)站
旅客9已經(jīng)進(jìn)站
觀察結(jié)果發(fā)現(xiàn),同一時(shí)刻最多只能有 3 位旅客占用閘機(jī)進(jìn)站,其他旅客需要等待其進(jìn)站后讓出閘機(jī)才能刷卡進(jìn)站。
至此,大家對(duì)信號(hào)量已經(jīng)有了初步的理解,接下來我們繼續(xù)豐富對(duì) Semaphore 工具類的認(rèn)識(shí)。
6. 其他方法介紹
除過上面代碼中使用的最基本的 acquire 方法和 release 方法之外,我們還需要掌握其他幾個(gè)核心方法的使用。下面逐個(gè)介紹。
- Semaphore(int permits, boolean fair)
上面的例子中使用了 Semaphore (int permits) 構(gòu)造方法。
此構(gòu)造方法也是用于創(chuàng)建信號(hào)量對(duì)象,第二個(gè)參數(shù)表示創(chuàng)建的信號(hào)量是否秉持公平競(jìng)爭(zhēng)特性。即對(duì)資源的申請(qǐng)使用嚴(yán)格按照申請(qǐng)的順序給予允許。
一般情況下,我們使用 Semaphore (int permits) 構(gòu)造方法就可以了。
- availablePermits()
返回當(dāng)前還可用的許可數(shù),即還允許多少個(gè)線程進(jìn)行使用資源。套用在上面的例子中,就是返回當(dāng)前還有多少臺(tái)閘機(jī)空閑可用。
int availablePermits = semaphore.availablePermits();
System.out.println("當(dāng)前可用閘機(jī)數(shù)" + availablePermits);
>>運(yùn)行結(jié)果:
當(dāng)前可用閘機(jī)數(shù)2
旅客0已經(jīng)占有一臺(tái)閘機(jī)
當(dāng)前可用閘機(jī)數(shù)1
旅客1已經(jīng)占有一臺(tái)閘機(jī)
......
- hasQueuedThreads()
返回是否有線程正在等待獲取資源。也就是返回當(dāng)前是否有人在排隊(duì)等待過閘機(jī)。
boolean hasQueuedThreads = semaphore.hasQueuedThreads();
System.out.println("當(dāng)前是否有旅客等待閘機(jī)進(jìn)站:"+hasQueuedThreads);
>>運(yùn)行結(jié)果:
當(dāng)前是否有旅客等待閘機(jī)進(jìn)站:false
旅客0已經(jīng)占有一臺(tái)閘機(jī)
當(dāng)前是否有旅客等待閘機(jī)進(jìn)站:false
旅客1已經(jīng)占有一臺(tái)閘機(jī)
當(dāng)前是否有旅客等待閘機(jī)進(jìn)站:false
旅客2已經(jīng)占有一臺(tái)閘機(jī)
- acquire(int permits)
申請(qǐng)指定數(shù)目的信號(hào)量許可,在獲取不到指定數(shù)目的許可時(shí)將一直阻塞。就好比一個(gè)旅客需要同時(shí)占用兩個(gè)閘機(jī)過站。類似的 release (int permits) 方法用于釋放指定數(shù)目的信號(hào)量許可。
acquire (int permits) 同上面例子中使用的 acquire () 最大的區(qū)別就是用于一次性申請(qǐng)多個(gè)許可,當(dāng)參數(shù) permits = 1 時(shí),兩者相同。release (int permits) 和 release () 也是類似。
- tryAcquire()
嘗試申請(qǐng)信號(hào)量許可,無論是否申請(qǐng)成功都返回申請(qǐng)結(jié)果。當(dāng)申請(qǐng)成功時(shí)返回 true , 否則返回 false 。程序里面根據(jù)申請(qǐng)結(jié)果決定后繼的處理流程。和 acquire () 的主要區(qū)別在于,不會(huì)阻塞立刻返回。
同類功能的方法還有 tryAcquire (int permits) 、tryAcquire (long timeout, TimeUnit unit) 、tryAcquire (int permits, long timeout, TimeUnit unit) 。這些方法實(shí)現(xiàn)的功能一樣,只是可以更加精細(xì)化地控制對(duì)資源申請(qǐng),比如申請(qǐng)超時(shí)控制、申請(qǐng)?jiān)S可數(shù)量。
7. 工具對(duì)比
大家可能有一個(gè)疑問了,Semaphore 好像和 synchronized 關(guān)鍵字沒什么區(qū)別,都可以實(shí)現(xiàn)同步。
其實(shí)不然,synchronized 真正用于并發(fā)控制,確保對(duì)某一個(gè)資源的串行訪問;而 Semaphore 限制訪問資源的線程數(shù),其實(shí)并沒有實(shí)現(xiàn)同步,只有當(dāng) Semaphore 限制的資源同時(shí)只允許一個(gè)線程訪問時(shí),兩者達(dá)到的效果一樣。
大家記住,Semaphore 和 synchronized 最主要的差別是 Semaphore 可以控制一個(gè)或多個(gè)并發(fā),而 synchronized 只能是一個(gè)。這一點(diǎn)需要大家好好琢磨。
還是通過上面的例子的運(yùn)行結(jié)果給大家做一下解釋。
>>運(yùn)行結(jié)果:
旅客0已經(jīng)占有一臺(tái)閘機(jī) <-------
旅客1已經(jīng)占有一臺(tái)閘機(jī) | 觀察發(fā)現(xiàn)同時(shí)有多個(gè)并發(fā)執(zhí)行,而非串行的一個(gè)旅客過完閘機(jī)后才輪到下一個(gè)旅客。
旅客2已經(jīng)占有一臺(tái)閘機(jī) <-------
......
8. 小結(jié)
本節(jié)通過一個(gè)簡(jiǎn)單的例子,介紹了 Semaphore 的基本用法。另外對(duì)一些核心方法做了簡(jiǎn)單介紹并給出應(yīng)用場(chǎng)景。希望大家在學(xué)習(xí)過程中,多思考勤練習(xí),早日掌握之。