Semaphore原理、實戰和原始碼分析
一 工作原理
Semaphore(計數訊號量),內部維護一組許可證,通過acquire方法獲取許可證,如果獲取不到,則阻塞;
通過release釋放許可,即新增許可證。
許可證其實是Semaphore中維護的一個volatile整型state變數,初始化的時候定義一個數量,獲取時減少,釋放時增加,
一直都是在操作state。
Semaphore內部基於AQS(同步框架)實現了公平或分公平兩種方式獲取資源。
Semaphore主要用於限制執行緒數量、一些公共資源的訪問。
下面通過例項體驗Semaphore的含義,然後在從原始碼角度分析(jdk1.8)Semaphore的實現原理。
二 實戰
下面舉一個獨特的例子,吃飯的時候千萬別看,看完怕吃不下飯。
我們公司每層樓都有一個衛生間,每個衛生間有5個大號坑,真心不夠用啊!
衛生間是公共資源,這裡用Semaphore來模擬現實的排隊上廁所這件事情。
1)通過acquire獲取鎖
衛生間有5個固定的坑,通過acquire來獲取坑,獲取到就用,如果沒有獲取到就阻塞,就憋著,排隊等待,
總不能踹開門把人家拽出來吧,我們都是文明人。
2)通過release釋放鎖
上完廁所的人通過release釋放坑,資源就讓出來了,然後排隊等待的人一個一個還是通過acquire去獲取資源上廁所,
獲取不到的還是耐心等待。
3)Semphore公平或非公平獲取資源
這個例子舉到這個地方,順便把Semphore的公平或不公平獲取資源分析下。
Semaphore兩個構造器:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
從程式碼可以清晰的看到,預設是非公平,fair傳true就是公平。
公平:就是一個人來了看到前面有人排隊,就老老實實的跟著排。
非公平:就是一個人來了看到一長串的人排隊,非得直接奔著坑去,拉下門,發現都有人,
然後老老實實的跟著排隊。
區別:公平就是看到有人直接老實排隊,非公平就是明明看到排隊了,非得先去試一下,然後再老老實實的排隊。
服務員,開始上菜!!!
首先定義一個廁所類,裡面通過Semaphore設定了5個坑:
package com.lanhuigu.demo9.Semaphore;
import java.util.concurrent.Semaphore;
/**
* 衛生間有5個坑
* @author yihonglei
* @date 2018/9/28 19:22
*/
public class Toilet {
/**
* 5個固定的茅坑
*/
private static Semaphore semaphore = new Semaphore(5, true);
/**
* 茅坑
*/
static class Pit {
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
/**
* 獲取一個坑
*/
public Pit getPit() throws InterruptedException {
semaphore.acquire();
Pit pit = new Pit();
pit.setDesc("獲得坑了!!!");
return pit;
}
/**
* 釋放一個坑
*/
public Pit releasePit() {
semaphore.release();
Pit pit = new Pit();
pit.setDesc("釋放了坑!!!");
return pit;
}
}
然後,定義一個上廁所的執行緒,表示誰上廁所:
package com.lanhuigu.demo9.Semaphore;
/**
* 大便!!!(畫面感很強!)
* @author yihonglei
* @date 2018/9/29 09:40
*/
public class ShiftThread implements Runnable {
private Toilet toilet;
private Integer num;
public ShiftThread(Toilet toilet, Integer num) {
this.toilet = toilet;
this.num = num;
}
@Override
public void run() {
try {
// 獲得坑
Toilet.Pit pitAcquire = toilet.getPit();
System.out.println("序號:" + num + pitAcquire.getDesc());
// 解決大號
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放坑
Toilet.Pit pitRelealse = toilet.releasePit();
System.out.println("序號:" + num + pitRelealse.getDesc());
}
}
}
最後,模擬下上廁所,上完,以及排隊等待的過程:
package com.lanhuigu.demo9.Semaphore;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Semaphore(計數訊號量)測試
* @author yihonglei
* @date 2018/9/24 23:12
*/
public class SemaphoreDemo {
public static void main(String[] args) {
try {
Toilet toilet = new Toilet();
Executor executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 30; i ++) {
executor.execute(new ShiftThread(toilet, i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
程式解釋:一開始5個坑都是空的,會有5個人先後獲取了坑,其它的耐心等待,當某一個空出來的時候,
再按排隊的順序接著上,然後就是重複獲取資源和釋放資源的過程,但是最多隻能同時有5個人在坑裡。
這就是控制對公共資源的訪問,因為很多資源是有限的,有限的資源就不能過度使用,否則就亂了,
你不能一個坑裡蹲兩人吧,如果有兩個人,哪絕對不是在上廁所,而是在幹別的,幹啥就不知道了,哈哈!
三 原始碼分析
1、從構造器開始看起
1)permits為傳入的許可證數,非公平構造器;
2)permits為傳入的許可證數,fair是boolean型的,如果傳入true,則公平,否則不公平;
預設使用的是非公平構造器。
NonfairSync和FairSync原始碼:
兩者都繼承了Sync同步器,初始化時都呼叫了父類構造器,同時都有一個獲取訊號的方法,稍後再分析獲取訊號的區別。
Sync原始碼:
1)Sync為Semaphore的內部靜態類,同時繼承了AQS同步器框架,主要是再獲取或釋放訊號的時候通過
同步器的CAS演算法實現原子更新。
2)構造器呼叫了setState方法,state為Semaphore的一個成員變數,對應setState方法原始碼如下:
/**
* The synchronization state.
*/
private volatile int state;
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
所以從Semaphore構造器傳進來的permits許可證數量,最後賦值到volatile變數state。
volatile是共享變數,記憶體可見,可用於執行緒間通訊,每個執行緒看到的state,一定會拿到最新的state值。
Semaphore獲取訊號或釋放訊號都是對state進行原子性減少或增加的操作。
2、acquire(獲取訊號量)
獲取訊號量預設方法原始碼:
acquire有其它的重構方法,咱們這裡分析預設獲取訊號量方法,其它的雷同。
獲取訊號量時,呼叫的是Sync的acquireSharedInterruptibly方法,預設引數為1,
Sync繼承了AQS,呼叫的其實是AQS的方法原始碼:
1)判斷當前來獲取訊號量的執行緒是否中斷,如果中斷,直接跑執行緒中斷異常。
2)tryAcauireShared是真正去獲取訊號量的方法,獲取到就返回當前訊號量剩餘數,也就是還有多少資源,
否則就返回-1。
3)如果獲取不到訊號量,tryAcauireShared方法返回-1,就會進入doAcquireSharedInterruptibly方法,
該方法會將哪些獲取不到訊號量的執行緒加入佇列裡面等待排隊。
咱們繼續看tryAcauireShared是如何處理訊號量獲取的,tryAcauireShared方法簽名:
該方法在Semaphore的靜態內部類中有兩個實現類:
先看公平的FairSync:
1)先判斷等待佇列裡面是否有正在等待獲取訊號的執行緒,如果有,獲取不到訊號量,就返回-1,外層程式碼會把
該執行緒加入等待佇列裡面,等待著獲取訊號量。
就好比上廁所,你看到有人排隊了,就不要去嘗試獲取資源了,老老實實排隊就行了,廁所肯定是滿位,要不然別人
也沒傻到有位置不用,閒得沒事在哪裡排隊玩。
這就是公平獲取訊號的邏輯,看到有排隊的,老老實實加入排隊大軍。
2)如果有資源,首先獲取可用的資源,然後減掉我們想要獲取的資源,得到剩餘的資源,也就是remaining。
判斷條件remaining<0是防止雖然沒有排隊的,但是資源剛好佔滿了,這個時候來獲取,必然沒有資源,可用為0,
remaining就是負數,直接返回負數,外層會把該執行緒加入等待佇列。
如果remaining是大於0的,則會執行後面的compareAndSetState(available,remaining)通過原子更新訊號量方式
來獲取訊號了,如果更新成功,獲取成功,返回true,這個時候返回的remaining就是大於0的,並且這個玩意就是
剩餘訊號量。所以,當能獲取訊號量時,返回的int值就是當前剩餘訊號量。
然後再看非公平NonfairSync原始碼:
有沒有發現,非公平相對於公平的程式碼只是去掉了關於等待佇列的判斷部分,非公平上來絕不判斷
佇列裡面是否有等待獲取訊號的執行緒,而是直接獲取資源,獲取不到外層處才老老實實的加入等待佇列。
就好比上廁所,大家都在排隊呢,一個哥們來了,看到排隊,不聽人說沒坑了,也不排隊,就是奔著資源去,
然後挨個門拉一遍,發現都有人,才又老老實實的去排隊。
小結下公平與非公平區別:
1)公平就是看到有執行緒等待獲取訊號了,就跟著排隊,不去試著獲取訊號;
2)非公平就是無視排隊,直接嘗試獲取訊號量,獲取不到再加入排隊大軍;
3、release(釋放訊號量)
釋放訊號量原始碼:
1)釋放訊號量。具體實現原始碼:
原始碼解析:
1)獲取當前訊號數量,也就是state變數的值。
2)當前訊號量加上要釋放的訊號量等於釋放後的訊號量。
3)next<current時拋異常,也就是釋放後的訊號量還不如釋放前多,要不是傳了負數值或者出現併發導致state為負數了。
4)通過CAS演算法原子操作訊號量state,進行訊號量釋放,恢復訊號量個數,也就是使state值增加releases個數。
2)如果tryReleaseShared嘗試釋放state成功,通過doReleaseShared進行後繼節點喚醒具體處理:
茅坑讓出來了,等著的人就可以用了!等著的人也得保證先來後到,程式處理就麻煩些!
1)獲取佇列的頭節點元素,如果不為null,並且不為尾節點,說白了,就是不止一個人等待,進入判斷。
2)如果執行緒節點是需要喚醒的執行緒,則進行喚醒,獲取資源使用。
3)失敗後重試。
4)如果沒有後繼需要喚醒的節點,則退出,就相當於每人排隊上廁所了,讓出來資源就空著。
四 Semaphore總結
1、Semaphore內部維護一組訊號量,即一個volatile的整型state變數。
2、Semaphore分為公平或非公平兩種方式,獲取訊號量或釋放訊號量的本質是對state進行
原子的減少或增加操作。
3、獲取不到訊號的執行緒放在等待佇列裡面,釋放訊號的時候會喚醒後繼節點。
4、Semaphore主要用於對執行緒數量、公共資源(比如資料庫連線池)等進行數量控制。