原文地址:cnblogs.com/liuyun1995/p/8474026.html 作者:勞夫子
Semaphore(訊號量)用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。
Semaphore提供了一個許可證的概念,可以把這個許可證看作公共汽車車票,只有成功獲取車票的人才能夠上車,並且車票是有一定數量的,不可能毫無限制的發下去,這樣就會導致公交車超載。所以當車票發完的時候(公交車以滿載),其他人就只能等下一趟車了。如果中途有人下車,那麼他的位置將會空閒出來,因此如果這時其他人想要上車的話就又可以獲得車票了。
建構函式
Semaphore類提供了2種建構函式,分別如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
複製程式碼
這兩個構造方法,都必須提供許可的數量,第二個構造方法可以指定是公平模式還是非公平模式,預設非公平模式。
在ReentrantLock中公平鎖和非公平鎖獲取鎖機制的差別:對於公平鎖而言,如果當前執行緒不在CLH佇列的頭部,則需要排隊等候,而非公平鎖則不同,它無論當前執行緒處於CLH佇列的何處都會直接獲取鎖。所以公平訊號量和非公平訊號量的區別也一樣。
獲取許可證
Semaphore類提供了4種獲取許可證的方法,分別如下:
//獲取一個許可證(響應中斷),在沒有可用的許可證時當前執行緒被阻塞。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//獲取一個許可證(不響應中斷)
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
//嘗試獲取許可證(非公平獲取),立即返回結果(非阻塞)。
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
//嘗試獲取許可證(定時獲取)
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
複製程式碼
下面我們一起看看,acquire()是如何獲取許可證的? 其原始碼如下:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
複製程式碼
acquireSharedInterruptibly方法首先就是去呼叫tryAcquireShared方法去嘗試獲取,tryAcquireShared在AQS裡面是抽象方法,FairSync和NonfairSync這兩個派生類實現了該方法的邏輯。FairSync實現的是公平獲取的邏輯,而NonfairSync實現的非公平獲取的邏輯。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
// 非公平方式嘗試獲取
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 獲取可用許可證
int available = getState();
// 獲取剩餘許可證
int remaining = available - acquires;
// 1.如果remaining小於0則直接返回remaining
// 2.如果remaining大於0則先更新同步狀態再返回remaining
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
// 非公平同步器
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
// 嘗試獲取許可證
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// 公平同步器
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
// 嘗試獲取許可證
protected int tryAcquireShared(int acquires) {
for (;;) {
// 判斷同步佇列前面存在排隊
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
複製程式碼
-
非公平獲取鎖的邏輯是先取出當前同步狀態(同步狀態表示許可證個數),將當前同步狀態減去參入的引數,如果結果不小於0的話證明還有可用的許可證,那麼就直接使用CAS操作更新同步狀態的值,最後不管結果是否小於0都會返回該結果值。
-
公平獲取鎖的邏輯僅僅是在此之前先去呼叫hasQueuedPredecessors方法判斷同步佇列是否存在排隊,如果有的話就直接返回-1表示獲取失敗,否則繼續執行和非公平獲取一樣的步驟。
釋放許可證
Semaphore類提供了2種釋放許可證方法,分別如下:
public void release() {
sync.releaseShared(1);
}
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
複製程式碼
呼叫release方法是釋放一個許可證,它的操作很簡單,就呼叫了AQS的releaseShared方法。
//釋放鎖的操作(共享模式)
public final boolean releaseShared(int arg) {
//1.嘗試去釋放鎖
if (tryReleaseShared(arg)) {
//2.如果釋放成功就喚醒其他執行緒
doReleaseShared();
return true;
}
return false;
}
複製程式碼
AQS的releaseShared方法首先呼叫tryReleaseShared方法嘗試釋放鎖,其實現邏輯在子類Sync裡面。
// 嘗試釋放操作
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 獲取當前同步狀態
int current = getState();
// 將當前同步狀態加上傳入的引數
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 以CAS方式更新同步狀態的值, 更新成功則返回true, 否則繼續迴圈
if (compareAndSetState(current, next))
return true;
}
}
複製程式碼
tryReleaseShared方法裡面採用for迴圈進行自旋,首先獲取同步狀態,將同步狀態加上傳入的引數,然後以CAS方式更新同步狀態,更新成功就返回true並跳出方法,否則就繼續迴圈直到成功為止。
動手寫個連線池
下面我們就來利用Semaphore實現一個簡單的資料庫連線池,通過這個例子希望讀者們能更加深入的掌握Semaphore的運用。
public class ConnectPool {
//連線池大小
private int size;
//資料庫連線集合
private Connect[] connects;
//連線狀態標誌
private boolean[] connectFlag;
//剩餘可用連線數
private volatile int available;
//訊號量
private Semaphore semaphore;
//構造器
public ConnectPool(int size) {
this.size = size;
this.available = size;
semaphore = new Semaphore(size, true);
connects = new Connect[size];
connectFlag = new boolean[size];
initConnects();
}
//初始化連線
private void initConnects() {
//生成指定數量的資料庫連線
for(int i = 0; i < this.size; i++) {
connects[i] = new Connect();
}
}
//獲取資料庫連線
private synchronized Connect getConnect(){
for(int i = 0; i < connectFlag.length; i++) {
//遍歷集合找到未使用的連線
if(!connectFlag[i]) {
//將連線設定為使用中
connectFlag[i] = true;
//可用連線數減1
available--;
System.out.println("【"+Thread.currentThread().getName()+"】以獲取連線 剩餘連線數:" + available);
//返回連線引用
return connects[i];
}
}
return null;
}
//獲取一個連線
public Connect openConnect() throws InterruptedException {
//獲取許可證
semaphore.acquire();
//獲取資料庫連線
return getConnect();
}
//釋放一個連線
public synchronized void release(Connect connect) {
for(int i = 0; i < this.size; i++) {
if(connect == connects[i]){
//將連線設定為未使用
connectFlag[i] = false;
//可用連線數加1
available++;
System.out.println("【"+Thread.currentThread().getName()+"】以釋放連線 剩餘連線數:" + available);
//釋放許可證
semaphore.release();
}
}
}
//剩餘可用連線數
public int available() {
return available;
}
}
複製程式碼
測試程式碼:
public class TestThread extends Thread {
private static ConnectPool pool = new ConnectPool(3);
@Override
public void run() {
try {
Connect connect = pool.openConnect();
Thread.sleep(100); //休息一下
pool.release(connect);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
new TestThread().start();
}
}
}
複製程式碼
使用一個陣列來存放資料庫連線的引用,在初始化連線池的時候會呼叫initConnects方法建立指定數量的資料庫連線,並將它們的引用存放到陣列中,此外還有一個相同大小的陣列來記錄連線是否可用。
每當外部執行緒請求獲取一個連線時,首先呼叫semaphore.acquire()方法獲取一個許可證,然後將連線狀態設定為使用中,最後返回該連線的引用。許可證的數量由構造時傳入的引數決定,每呼叫一次semaphore.acquire()方法許可證數量減1,當數量減為0時說明已經沒有連線可以使用了,這時如果其他執行緒再來獲取就會被阻塞。每當執行緒釋放一個連線的時候會呼叫semaphore.release()將許可證釋放,此時許可證的總量又會增加,代表可用的連線數增加了,那麼之前被阻塞的執行緒將會醒來繼續獲取連線,這時再次獲取就能夠成功獲取連線了。
歡迎留言補充,共同交流。個人微信公眾號求關注: