深入淺出 Java Concurrency (12): 鎖機制 part 7 訊號量(Semaphore)

來源:互聯網
上載者:User

標籤:archive   www   排除   package   使用   countdown   jdk   就會   res   

 

Semaphore 是一個計數訊號量。從概念上講,訊號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然後再擷取該許可。每個 release() 添加一個許可,從而可能釋放一個正在阻塞的擷取者。但是,不使用實際的許可對象,Semaphore 只對可用許可的號碼進行計數,並採取相應的行動。

說白了,Semaphore是一個計數器,在計數器不為0的時候對線程就允許存取,一旦達到0,那麼所有請求資源的新線程都會被阻塞,包括增加請求到許可的線程,也就是說Semaphore不是可重新進入的。每一次請求一個許可都會導致計數器減少1,同樣每次釋放一個許可都會導致計數器增加1,一旦達到了0,新的許可請求線程將被掛起。

緩衝池整好使用此思想來實現的,比如連結池、對象池等。

清單1 對象池

package xylz.study.concurrency.lock;

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ObjectCache<T> {

    public interface ObjectFactory<T> {

        T makeObject();
    }

    class Node {

        T obj;

        Node next;
    }

    final int capacity;

    final ObjectFactory<T> factory;

    final Lock lock = new ReentrantLock();

    final Semaphore semaphore;

    private Node head;

    private Node tail;

    public ObjectCache(int capacity, ObjectFactory<T> factory) {
        this.capacity = capacity;
        this.factory = factory;
        this.semaphore = new Semaphore(this.capacity);
        this.head = null;
        this.tail = null;
    }

    public T getObject() throws InterruptedException {
        semaphore.acquire();
        return getNextObject();
    }

    private T getNextObject() {
        lock.lock();
        try {
            if (head == null) {
                return factory.makeObject();
            } else {
                Node ret = head;
                head = head.next;
                if (head == null) tail = null;
                ret.next = null;//help GC
                return ret.obj;
            }
        } finally {
            lock.unlock();
        }
    }

    private void returnObjectToPool(T t) {
        lock.lock();
        try {
            Node node = new Node();
            node.obj = t;
            if (tail == null) {
                head = tail = node;
            } else {
                tail.next = node;
                tail = node;
            }

        } finally {
            lock.unlock();
        }
    }

    public void returnObject(T t) {
        returnObjectToPool(t);
        semaphore.release();
    }

}

清單1描述了一個基於訊號量Semaphore的對象池實現。此對象池最多支援capacity個對象,這在建構函式中傳入。對象池有一個基於FIFO的隊列,每次從對象池的頭結點開始取對象,如果頭結點為空白就直接構造一個新的對象返回。否則將頭結點對象取出,並且頭結點往後移動。特別要說明的如果對象的個數用完了,那麼新的線程將被阻塞,直到有對象被返回回來。返還對象時將對象加入FIFO的尾節點並且釋放一個閒置訊號量,表示對象池中增加一個可用對象。

實際上對象池、線程池的原理大致上就是這樣的,只不過真正的對象池、線程池要處理比較複雜的邏輯,所以實現起來還需要做很多的工作,例如逾時機制,自動回收機制,對象的有效期間等等問題。

這裡特別說明的是訊號量只是在訊號不夠的時候掛起線程,但是並不能保證訊號量足夠的時候擷取對象和返還對象是安全執行緒的,所以在清單1中仍然需要鎖Lock來保證並發的正確性。

將訊號量初始化為 1,使得它在使用時最多隻有一個可用的許可,從而可用作一個相互排斥的鎖。這通常也稱為二進位訊號量,因為它只能有兩種狀態:一個可用的許可,或零個可用的許可。按此方式使用時,二進位訊號量具有某種屬性(與很多 Lock 實現不同),即可以由線程釋放“鎖”,而不是由所有者(因為訊號量沒有所有權的概念)。在某些專門的上下文(如死結恢複)中這會很有用。

上面這段話的意思是說當某個線程A持有訊號量數為1的訊號量時,其它線程只能等待此線程釋放資源才能繼續,這時候持有訊號量的線程A就相當於持有了“鎖”,其它線程的繼續就需要這把鎖,於是線程A的釋放才能決定其它線程的運行,相當於扮演了“鎖”的角色。

 

另外同公平鎖非公平鎖一樣,訊號量也有公平性。如果一個訊號量是公平的表示線程在擷取訊號量時按FIFO的順序得到許可,也就是按照請求的順序得到釋放。這裡特別說明的是:所謂請求的順序是指在請求訊號量而進入FIFO隊列的順序,有可能某個線程先請求訊號而後進去請求隊列,那麼次線程擷取訊號量的順序就會晚於其後請求但是先進入請求隊列的線程。這個在公平鎖和非公平鎖中談過很多。

 

除了acquire以外,Semaphore還有幾種類似的acquire方法,這些方法可以更好的處理中斷和逾時或者非同步等特性,可以參考JDK API。

按照同樣的學習原則,下面對主要的實現進行分析。Semaphore的acquire方法實際上訪問的是AQS的acquireSharedInterruptibly(arg)方法。這個可以參考CountDownLatch一節或者AQS一節。

所以Semaphore的await實現也是比較簡單的。與CountDownLatch不同的是,Semaphore區分公平訊號和非公平訊號。

清單2 公平訊號擷取方法

protected int tryAcquireShared(int acquires) {
    Thread current = Thread.currentThread();
    for (;;) {
        Thread first = getFirstQueuedThread();
        if (first != null && first != current)
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

清單3 非公平訊號擷取方法

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

對比清單2和清單3可以看到,公平訊號和非公平訊號在於第一次嘗試能否擷取訊號時,公平訊號量總是將當前線程進入AQS的CLH隊列進行排隊(因為第一次嘗試時隊列的頭結點線程很有可能不是當前線程,當然不排除同一個線程第二次進入訊號量),從而根據AQS的CLH隊列的順序FIFO依次擷取訊號量;而對於非公平訊號量,第一次立即嘗試能否拿到訊號量,一旦訊號量的剩餘數available大於請求數(acquires通常為1),那麼線程就立即得到了釋放,而不需要進行AQS隊列進行排隊。只有remaining<0的時候(也就是訊號量不夠的時候)才會進入AQS隊列。

所以非公平訊號量的輸送量總是要比公平訊號量的輸送量要大,但是需要強調的是非公平訊號量和非公平鎖一樣存在“饑渴死”的現象,也就是說活躍線程可能總是拿到訊號量,而非活躍線程可能難以拿到訊號量。而對於公平訊號量由於總是靠請求的線程的順序來擷取訊號量,所以不存在此問題。

 

 參考資料:

    1.  訊號量(Semaphore)在生產者和消費者模式的使用
    2. What is mutex and semaphore in Java ? What is the main difference ?
    3. 關於 java.util.concurrent 您不知道的 5 件事,第 2 部分
    4. Semahores

深入淺出 Java Concurrency (12): 鎖機制 part 7 訊號量(Semaphore)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.