文章目錄
前言
Semaphore在多線程協作中常常用來控制公共資源的訪問,限制同時訪問數量。
打個比方,Semaphore就像是一個裝有令牌(permit
)的黑箱子,拿到令牌的人才能去做愛做的事情,誰都可以從裏面拿走若干令牌,誰都可以把新的令牌扔到裏面去,但Semaphore從來不記載誰拿走的令牌。
重要成員
和CountDownLatch一樣,Semaphore依賴的也是AQS的共享鎖,核心屬性也是AQS的state成員。在Semaphore中,使用內部類繼承AQS使用。
構造器
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
默認構造器使用非公平鎖,可以通過參數fair
來得到公平鎖。參數permits
最終會賦值給AQS的state成員。
獲取信號量(減小state)
Semaphore方法 | 調用的AQS方法 | 是否阻塞 | 是否響應中斷 | 是否超時機制 | 返回值及含義 |
---|---|---|---|---|---|
acquire() | sync.acquireSharedInterruptibly(1) | ✓ | ✓ | - | void |
acquire(int permits) | sync.acquireSharedInterruptibly(permits) | ✓ | ✓ | - | void |
acquireUninterruptibly() | sync.acquireShared(1) | ✓ | - | - | void |
acquireUninterruptibly(int permits) | sync.acquireShared(permits) | ✓ | - | - | void |
tryAcquire(long timeout, TimeUnit unit) | sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)) | ✓ | ✓ | ✓ | boolean 返回時是否獲得了鎖 |
tryAcquire(int permits, long timeout, TimeUnit unit) | sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout)) | ✓ | ✓ | ✓ | boolean 返回時是否獲得了鎖 |
tryAcquire() | sync.nonfairTryAcquireShared(1) >= 0 | - | boolean 返回時是否獲得了鎖 |
- 上面表格中,所有沒有參數的Semaphore方法,實質上都是在獲得單位1的信號量。
- 調用的AQS方法,其實只有三種,
acquireShared
(共享鎖的獲取與釋放中已經進行了講解),acquireSharedInterruptibly
和tryAcquireSharedNanos
(CountDownLatch源碼解析中已經進行了講解)。這三者的區別已經在表格列出。- 需要響應中斷,方法聲明會拋出中斷異常。
- 有超時機制,就需要用返回值區別 獲得鎖返回 和 超時都沒獲得到鎖 兩種情況。
- 這三個方法都需要調用到AQS子類實現的
tryAcquireShared
,該方法用來獲取共享鎖,子類可以將其實現公平鎖或是非公平鎖。
nonfairTryAcquireShared
其實不大應該放在上表裏面,因爲它根本不是AQS的方法,只是AQS子類的新加方法。因爲它根本沒有阻塞等待的過程,只是簡單的try一次,成功失敗聽天由命,所以它根本不會阻塞。
表格中,帶permits
參數的方法可以獲得單位不爲1的信號量,但是方法中對permits
參數的檢查要求是>=0
,也就是說,允許線程獲得0單位的信號量,雖然我感覺這樣沒有任何意義。
這裏我們再複習一下tryAcquireShared
返回值的含義:
- 如果返回值大於0,說明獲取共享鎖成功,並且後續獲取也可能獲取成功。
- 如果返回值等於0,說明獲取共享鎖成功,但後續獲取可能不會成功。
- 如果返回值小於0,說明獲取共享鎖失敗。
非公平實現的tryAcquireShared
//NonfairSync
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
//Sync
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
- 方法使用了自旋,這是合理且必要的。共享鎖是共享的,自然可能有多個線程正在同時執行上面的代碼,即使失敗了也不能退出循環,而是應該失敗後再得到當前值,然後再次CAS嘗試。
- 如果
remaining
算出來小於0,說明剩餘信號量已經不夠拿的了,那就直接返回remaining
這個負數(表達獲取共享鎖失敗),不做CAS操作。 - 如果
remaining
算出來大於等於0,說明剩餘信號量夠拿的,緊接着如果CAS設置成功,就返回remaining
這個大於等於0的數(表達獲取共享鎖成功)。 - 這個方法想要退出,只有當前線程拿到了想要數量的信號量,或剩餘信號量已經不夠拿。
公平實現的tryAcquireShared
//FairSync
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;
}
}
區別就是要先判斷同步隊列中是否已經有節點了(hasQueuedPredecessors
),如果有那同步隊列中的節點屬於是排在當前線程之前的,所以只好直接返回-1。
釋放信號量(增加state)
釋放信號量就不用區別什麼公平不公平了。
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;
}
}
- 考慮多線程,使用自旋保證
releases
單位的信號量能夠釋放到位。 - 只有CAS設置成功,或溢出int型的範圍,才能退出這個循環。
工具方法
tryAcquire
上面表格裏,一直沒有好好講tryAcquire
,因爲它在裏面屬於一個異類,沒有阻塞等待的過程。
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
而nonfairTryAcquireShared
的實現也看過了,裏面根本沒有調用過LockSupport.park
,確實沒有阻塞。不要被自旋所迷惑,自旋也不是阻塞,而且這個自旋過程一般情形下很快就會結束。
簡而言之,Semaphore#tryAcquire
的作用就是嘗試 一次性的、非公平的 獲得鎖動作。注意這種一次性動作一定要是非公平實現的,不然大部分情況下(同步隊列中只要有一個線程在等待),這種一次性動作肯定不能成功。這也是爲什麼要把非公平實現放到NonfairSync
和FairSync
的父類裏的一個公共方法裏。
注意,返回值進行了處理,如果獲得共享鎖成功(nonfairTryAcquireShared
返回值>=0
),返回true;如果獲得共享鎖失敗(nonfairTryAcquireShared
返回值<0
),返回false。
reducePermits
//Semaphore
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
//Sync
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;
}
}
- 這個方法的作用也是獲得信號量,只不過這個函數相比
nonfairTryAcquireShared
的實現,它允許改變後的信號量是負數。 - 自旋的過程。想要退出函數,只有CAS操作成功或者向下溢出了。
if (next > current)
分支進入的原因只能是int型變量向下溢出,因爲reductions
被保證是一個>=0
的數。- 是
protected
方法,用起來不是那麼方便。
//Sync
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
放一下nonfairTryAcquireShared
的實現作對比。
drainPermits
//Semaphore
public int drainPermits() {
return sync.drainPermits();
}
//Sync
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
自旋的過程,直到信號量被清空爲0。
總結
- Semaphore的AQS子類是一個很標準的共享鎖實現。
- 獲得信號量 == 減小AQS的state。
- 釋放信號量 == 增加AQS的state。
- 共享鎖的AQS子類實現方法需要自旋,這一點在Semaphore和CountDownLatch都有體現。獨佔鎖的AQS子類實現方法不需要自旋。
- 獲得信號量失敗一定是因爲信號量已經減到0了,且獲得失敗就會阻塞。