多執行緒基礎(十九):Semaphore原始碼分析

冬天裡的懶貓發表於2020-11-12

1.類結構及註釋

1.1 類結構

Semaphore是基於AQS實現的訊號量,這個類主要用於控制執行緒的訪問數,或者對併發的數量進行控制。以將資源的被獲取方的速度限制在特定的值內。
其類結構如下:
image.png

其內部有持有基於AQS的Sync類,Sync類有FairSyn和NonfairSync兩個類來實現公平和非公平鎖。

1.2 註釋部分

Semaphore是一個用於計數的訊號量,從概念上講,訊號量實際上是一組許可證,每個acquire都需要獲得許可證才能執行。而每個release方法則會將之前獲取的許可證釋放。但是,再Semaphore中,並沒有實際的許可證物件存在,Semaphore只是保留了一個用於計數的數量並進行相應的加減操作。
訊號量通常用於限制執行緒數量,而不能訪問某些資源。例如,如下是一個使用訊號量控制專案pool訪問的類。

class Pool {
    private static final int MAX_AVAILABLE = 100;
    private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
 
    public Object getItem() throws InterruptedException {
     available.acquire();
      return getNextAvailableItem();
    }
 
    public void putItem(Object x) {
      if (markAsUnused(x))
        available.release();
    }
 
    // Not a particularly efficient data structure; just for demo
 
    protected Object[] items = ... whatever kinds of items being managed
    protected boolean[] used = new boolean[MAX_AVAILABLE];
 
    protected synchronized Object getNextAvailableItem() {
      for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (!used[i]) {
           used[i] = true;
           return items[i];
        }
      }
      return null; // not reached
    }
 
   protected synchronized boolean markAsUnused(Object item) {
      for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (item == items[i]) {
           if (used[i]) {
             used[i] = false;
             return true;
           } else
             return false;
        }
      }
      return false;
    }
  }}

在獲取之前,每個執行緒必須從訊號量獲取許可,以確保每個獲取項是可以使用的。當執行緒完成該獲取項之後,它將返回到pool中,並向訊號量返回一個許可。從而允許另一個執行緒獲取該項。需要注意的是,在呼叫acquire時,不會保持任何鎖同步,因為這將阻止某個項返回到pool中,訊號量封裝了限制訪問池所需的同步,與維護池本身一致性所需的任何同步分開。
初始化一個訊號量可以用作互斥鎖,這樣的話該訊號量最多隻有一個許可。這通常被稱為二進位制訊號量,因為它只有兩種狀態,一個許可可用,或者沒有許可可用。當以這種方式使用時,二進位制訊號量具有鎖定屬性,這與許多Lock的實現不同。這個可以由所有者之外的執行緒釋放,因為訊號量本身沒有所有權概念。這在某些特殊情況下(如死鎖恢復)會很有用。
此類的建構函式可以選擇公平引數fairness。設定為false時,此類不保證執行緒獲得許可的順序。特別是允許插隊,也就是說,可以在正在等待的執行緒之前為呼叫acquire的執行緒分配一個許可-從邏輯上講,新執行緒將自己置於該執行緒的頭部等待執行緒的佇列。當公平性設定為true時,訊號量可確保選擇呼叫任何acquire()方法的執行緒以處理它們呼叫這些方法的順序獲得許可(先進先出; FIFO)。請注意,FIFO排序必須適用於這些方法中的特定內部執行點。因此,一個執行緒有可能在另一個執行緒之前調acquisition,但在另一個執行緒之後到達排序點,並且類似地從該方法返回時也是如此。另請注意,未定時的tryAcquire方法不遵循公平性設定,但會採用任何可用的許可。
通常,用於控制資源訪問的訊號量應初始化為公平,以確保沒有執行緒因訪問資源而捱餓。當使用訊號量進行其他型別的同步控制時,非公平排序的吞吐量優勢通常會超過公平考慮。
此類還提供了方便的方法來同時acquire(int)獲取和release(int)釋放多個許可。當在沒有設定公平的情況下使用這些方法時,請注意無限期推遲的風險增加。
記憶體一致性分析:呼叫release()之類的“釋出”方法之前,執行緒中的操作發生在成功的“獲取”方法(如acquire()之前)在另一個執行緒中。

2.內部類Sync

與眾多Lock的實現一樣,Semaphore也是基於AQS實現的。在其內部有Sync類。Sync繼承了AbstractQueueSynhronizer。

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;

    Sync(int permits) {
        setState(permits);
    }

    final int getPermits() {
        return getState();
    }
}

其實現的方法如下:

2.1 nonfairTryAcquireShared

非公平鎖的模式下嘗試獲取共享鎖。

final int nonfairTryAcquireShared(int acquires) {
    //死迴圈
    for (;;) {
        //獲得state狀態
        int available = getState();
        //用當前的可用state的值減去acquires
        int remaining = available - acquires;
        //如果前面減去的結果小於0,或者cas的方式設定state成功,則退出
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

2.2 tryReleaseShared

釋放共享鎖

protected final boolean tryReleaseShared(int releases) {
   //死迴圈
    for (;;) {
        //獲得當前的狀態
        int current = getState();
        //next為當前state+release的值
        int next = current + releases;
        //如果next比current小,那麼只有一種可能就是越界,導致這個結果為負數
        if (next < current) // overflow
            //丟擲異常
            throw new Error("Maximum permit count exceeded");
        如果沒有異常,則cas設定next 成功則為true
        if (compareAndSetState(current, next))
            return true;
    }
}

2.3 reducePermits

減少許可證

final void reducePermits(int reductions) {
    //死迴圈
    for (;;) {
        //獲得當前的state
        int current = getState();
        //next為當前的state減去應該減少的reductions
        int next = current - reductions;
        //如果next任然大於current則說明沒有這麼多許可證,此時丟擲異常
        if (next > current) // underflow
            throw new Error("Permit count underflow");
        //採用cas修改
        if (compareAndSetState(current, next))
            return;
    }
}

2.4 drainPermits

將許可證清空

final int drainPermits() {
    //死迴圈
    for (;;) {
        //current為當前的state
        int current = getState();
        //如果current為0或者cas將current設定為0
        if (current == 0 || compareAndSetState(current, 0))
            //返回
            return current;
    }
}

3.NonfairSync與FairSync

3.1 NonfairSync

NonfairSync是Sync的非公平鎖的實現,其原始碼如下:

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);
    }
}

實際上只需要實現一個關鍵的方法,tryAcquireShared。這個方法呼叫了前面在sync中定義的nonfairTryAcquireShared方法。

3.2 FairSync

FairSync是Sync的公平鎖的實現,其原始碼如下:

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;

    FairSync(int permits) {
        super(permits);
    }

    protected int tryAcquireShared(int acquires) {
       //死迴圈
        for (;;) {
            //判斷佇列中是否存在前任節點,如果存在,返回-1,將當前執行緒阻塞
            if (hasQueuedPredecessors())
                return -1;
            //available為當前state
            int available = getState();
            //remaing為available減去acquires
            int remaining = available - acquires;
            //如果remaining小於0 或者cas設定為0 則返回
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

4.構造方法

4.1 Semaphore(int permits)

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

預設情況下建立了一個非公平鎖。

4.2 Semaphore(int permits, boolean fair)

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

建立的時候可以指定公平性,所謂非公平鎖,在Semaphore中,就是當執行緒獲得鎖的時候,可以嘗試進行一次插隊,如果不成功再進行排隊。那麼公平鎖的話,就是一上來就排隊。

5.其他方法

5.1 acquire

從Semaphore中獲得許可,阻塞,直到獲取到可用的許可證為止。或者執行緒被中斷。
如果能及時獲取一個許可,那麼減少這個許可的數量。
如果沒有可用的許可,則出於執行緒排程目的,當前執行緒將被禁用,並處於休眠狀態,直到發生以下兩種情況之一:
其他一些執行緒為此訊號量呼叫{release方法,接下來將為當前執行緒分配許可;或某些其他執行緒interrupt當前執行緒。

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

5.2 acquireUninterruptibly

獲取一個許可,阻塞,直到許可可用。不支援中斷。
如果能及時獲取一個許可,那麼減少這個許可的數量。
如果沒有可用的許可,則當前執行緒將出於執行緒排程目的而被禁用,並處於休眠狀態,直到某個其他執行緒為此訊號量呼叫release方法,然後將為當前執行緒分配許可。
如果當前執行緒在等待許可時被interrupt中斷,則它將繼續等待,但是與沒有許可的情況相比,為該執行緒分配許可的時間可能會有所變化。發生中斷。當執行緒確實從該方法返回時,將設定其中斷狀態。

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

5.3 tryAcquire

僅在呼叫時可用時,才從此訊號量獲取許可。如果許可證可用,則獲取許可證並立即返回,其值為true,從而將可用許可證數量減少一個。
如果沒有許可,則此方法將立即返回值false。
即使已將此訊號量設定為使用公平的排序策略,無論是否有其他執行緒正在等待,對tryAcquire的呼叫都可能會立即獲得許可。這種插入是指,即使再某些情況下,這種行為也會破壞公平性。如果要遵守公平性設定,請使用等效的tryAcquire(long,TimeUnit)和tryAcquire(0,TimeUnit.SECONDS),這些方法還會檢測中斷。

public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
}

5.4 tryAcquire(long timeout, TimeUnit unit)

如果在給定的等待時間內可用,並且當前執行緒尚未被中斷interrupt ,則從此訊號量獲取許可。
如果許可證可用,則獲取許可證並立即返回,其值為true,從而將可用許可證數量減少一個。
如果沒有可用的許可,則出於執行緒排程的目的,當前執行緒將被禁用,並處於休眠狀態,直到發生以下三種情況之一:

  • 其他一些執行緒為此訊號量呼叫release方法,接下來將為當前執行緒分配許可;
  • 或其他某個執行緒interrupt當前執行緒;
  • 或經過了指定的等待時間。
    如果經過了許可,則返回true,如果當前執行緒,在進入此方法時已設定其中斷狀態;或在等待獲得許可interrupt被中斷,
    將引發InterruptedException並清除當前執行緒的中斷狀態。
    如果經過了指定的等待時間,則返回值false。如果時間小於或等於零,則該方法將根本不等待。
public boolean tryAcquire(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

5.5 release

釋放一個許可證,將其返回給訊號量。
發放許可證,將可用許可證數量增加一個。如果有任何執行緒試圖獲取許可,則選擇一個執行緒並授予剛剛釋放的許可。出於執行緒排程目的而啟用(重新)該執行緒。
不要求釋放許可證的執行緒必須通過呼叫acquire獲得該許可證。通過在應用程式中程式設計約定,可以正確使用訊號量。

public void release() {
    sync.releaseShared(1);
}

5.6 acquire(int permits)

從此訊號量獲取給定數量的許可,阻塞直到所有許可都可用,或者執行緒interrupt。
這個方法可以一次性獲得多個許可證,如果這些許可是可用的,則及時返回。減少許可的可用總數。

如果沒有足夠的許可,則出於執行緒排程的目的,當前執行緒將被禁用,並處於休眠狀態,直到發生以下兩種情況之一:

  • 其他一些執行緒為此訊號量呼叫release方法之一,接下來將為當前執行緒分配許可,並且可用許可的數量可以滿足此請求;- 某些其他執行緒 interrupt 當前執行緒。

如果當前執行緒,在進入此方法時已設定其中斷狀態;或在等待許可時被中斷,將丟擲InterruptedException,並清除中斷狀態。
相反,將要分配給該執行緒的所有許可都分配給其他嘗試獲取許可的執行緒,就像通過呼叫release使許可可用一樣。

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

5.7 acquireUninterruptibly(int permits)

從該訊號量獲取給定數量的許可,直到所有條件都可用為止都將被阻止。
獲取給定數量的許可(如果有),然後立即返回,將可用許可的數量減少給定數量。如果沒有足夠的許可,則當前執行緒出於執行緒排程目的而被禁用,並處於休眠狀態,直到其他執行緒呼叫此訊號量的release方法之一,當前執行緒將被分配許可,並且可用許可的數量可以滿足該請求。
如果當前執行緒在等待許可時interrupt被中斷,則它將繼續等待,並且其在佇列中的位置不受影響。當執行緒確實從該方法返回時,將設定其中斷狀態。

public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

5.8 tryAcquire(int permits)

僅在呼叫時所有可用的情況下,從此訊號量獲取給定的許可數。如果有許可,則獲取給定的許可數,並立即返回,值為{@code true},從而減少了給定數量的可用許可證。
如果沒有足夠的許可證,則此方法將立即返回false,並且可用許可證的數量不變。
即使已將此訊號量設定為使用公平的排序策略,無論是否有其他執行緒正在等待,對tryAcquire的呼叫都會立即獲得許可。即使破壞公平性,這種插入行為在某些情況下也可能有用。如果要遵守公平性設定,請使用幾乎等效的tryAcquire(int,long,TimeUnit)tryAcquire(permits,0,TimeUnit.SECONDS)它還會檢測到中斷。

public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0;
}

5.9 tryAcquire(int permits, long timeout, TimeUnit unit)

如果所有訊號都在給定的等待時間內可用,並且當前執行緒尚未interrupt中斷,則從此訊號量獲取給定數量的許可。
如果有可用許可證,則獲取給定數量的許可證,並立即返回,其值為true,從而將可用許可證的數量減少給定數量。
如果沒有足夠的許可,則出於執行緒排程的目的,當前執行緒將被禁用,並且將處於休眠狀態,直到發生以下三種情況之一:

  • 其他一些執行緒為此訊號量呼叫release方法之一,接下來將為當前執行緒分配許可,並且可用許可的數量可以滿足此請求;- 要麼其他一些執行緒interrupt interrupts當前執行緒;
  • 要麼經過指定的等待時間。

如果獲得許可,則返回值 true。
如果當前執行緒:
在進入此方法時已設定其中斷狀態;或在等待獲取許可時interrupt被中斷,
然後將引發 InterruptedException並清除當前執行緒的中斷狀態。相反,將要分配給該執行緒的所有許可,都分配給其他嘗試獲取許可的執行緒,就好像通過呼叫release()使許可可用一樣。如果經過了指定的等待時間,則返回值false。如果時間小於或等於零,則該方法根本不會等待。將分配給此執行緒的任何許可證,而是分配給其他嘗試獲取的執行緒許可,就像通過呼叫release()來獲得許可一樣。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
    throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

5.10 release(int permits)

釋放給定數量的許可證,將其返回到訊號。
釋放給定數量的許可證,將可用許可證的數量增加該數量。如果有任何執行緒試圖獲取許可,則選擇一個執行緒並給出剛剛釋放的許可。如果可用許可的數量滿足該執行緒的請求,則出於執行緒排程的目的,(重新)啟用該執行緒。如果滿足該執行緒的請求後仍然有可用的許可,則將這些許可依次分配給其他嘗試獲取許可的執行緒。
無需要求釋放許可的執行緒必須通過呼叫acquire獲得許可。通過在應用程式中程式設計約定,可以正確使用訊號量。

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

6.總結

本文對Semaphore的原始碼進行了分析,Semphore類似於交通控制的訊號燈,通過許可證的方式,對競爭的資源的併發程度進行了控制。

相關文章