Semaphore詳解及代碼示例

1.什麼是Semaphore?
Semaphore是JDK提供的一個同步工具,它通過維護若干個許可證來控制線程對共享資源的訪問。 如果許可證剩餘數量大於零時,線程則允許訪問該共享資源;如果許可證剩餘數量爲零時,則拒絕線程訪問該共享資源。 Semaphore所維護的許可證數量就是允許訪問共享資源的最大線程數量。 所以,線程想要訪問共享資源必須從Semaphore中獲取到許可證。

2.Semaphore有哪些常用方法?
有acquire方法和release方法。 當調用acquire方法時線程就會被阻塞,直到Semaphore中可以獲得到許可證爲止,然後線程再獲取這個許可證。 當調用release方法時將向Semaphore中添加一個許可證,如果有線程因爲獲取許可證被阻塞時,它將獲取到許可證並被釋放;如果沒有獲取許可證的線程, Semaphore只是記錄許可證的可用數量。

3.Semaphore應用場景舉例
張三、李四和王五和趙六4個人一起去飯店喫飯,不過在特殊時期洗手很重要,飯前洗手也是必須的,可是飯店只有2個洗手池,洗手池就是不能被同時使用的公共資源,這種場景就可以用到Semaphore。

4.demo示例

創建顧客類:

package onemore.study.semaphore;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class Customer implements Runnable {
    private Semaphore washbasin;
    private String name;

    public Customer(Semaphore washbasin, String name) {
        this.washbasin = washbasin;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
            Random random = new Random();

            washbasin.acquire();
            System.out.println(
            	sdf.format(new Date()) + " " + name + " 開始洗手...");
            Thread.sleep((long) (random.nextDouble() * 5000) + 2000);
            System.out.println(
            	sdf.format(new Date()) + " " + name + " 洗手完畢!");
            washbasin.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然後,寫一個測試類模擬一下他們洗手的過程:

package onemore.study.semaphore;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;

public class SemaphoreTester {
    public static void main(String[] args) throws InterruptedException {
        //飯店裏只用兩個洗手池,所以初始化許可證的總數爲2。
        Semaphore washbasin = new Semaphore(2);

        List<Thread> threads = new ArrayList<>(3);
        threads.add(new Thread(new Customer(washbasin, "張三")));
        threads.add(new Thread(new Customer(washbasin, "李四")));
        threads.add(new Thread(new Customer(washbasin, "王五")));
        threads.add(new Thread(new Customer(washbasin, "趙六")));
        for (Thread thread : threads) {
            thread.start();
            Thread.sleep(50);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }
}

運行以後的結果應該是這樣的:

06:51:54.416 李四 開始洗手...
06:51:54.416 張三 開始洗手...
06:51:57.251 張三 洗手完畢!
06:51:57.251 王五 開始洗手...
06:51:59.418 李四 洗手完畢!
06:51:59.418 趙六 開始洗手...
06:52:02.496 王五 洗手完畢!
06:52:06.162 趙六 洗手完畢!

可以看到,當已經有兩個人在洗手的時候,其他人就被阻塞,直到有人洗手完畢纔是開始洗手。

5.Semaphore的內部原理
Semaphore內部主要通過AQS(AbstractQueuedSynchronizer)實現線程的管理。Semaphore在構造時,需要傳入許可證的數量,它最後傳遞給了AQS的state值。線程在調用acquire方法獲取許可證時,如果Semaphore中許可證的數量大於0,許可證的數量就減1,線程繼續運行,當線程運行結束調用release方法時釋放許可證時,許可證的數量就加1。如果獲取許可證時,Semaphore中許可證的數量爲0,則獲取失敗,線程進入AQS的等待隊列中,等待被其它釋放許可證的線程喚醒。

6.Semaphore的公平鎖與非公平鎖,以及實現原理
在上面的代碼中。這4個人會按照線程啓動的順序洗手嗎?答案是否定的,有可能趙六比王五先洗手,原因就在於使用的是下面這個構造函數:

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

這個類不保證線程獲得許可證的順序,調用acquire方法的線程可以在一直等待的線程之前獲得一個許可證。那麼如何保證線程的順序呢?可以使用Semaphore的另一個構造函數:

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

調用時傳入true,就可以保證線程的執行順序了:

Semaphore washbasin = new Semaphore(2, true);

在NonfairSync中,acquire方法核心源碼是:

final int nonfairTryAcquireShared(int acquires) {
	//acquires參數默認爲1,表示嘗試獲取1個許可證。
    for (;;) {
        int available = getState();
        //remaining是剩餘的許可數數量。
        int remaining = available - acquires;
        //剩餘的許可數數量小於0時,
        //當前線程進入AQS中的doAcquireSharedInterruptibly方法
        //等待可用許可證並掛起,直到被喚醒。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

release方法核心源碼是:

protected final boolean tryReleaseShared(int releases) {
	//releases參數默認爲1,表示嘗試釋放1個許可證。
    for (;;) {
        int current = getState();
        //next是如果許可證釋放成功,可用許可證的數量。
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        //如果許可證釋放成功,
        //當前線程進入到AQS的doReleaseShared方法,
        //喚醒隊列中等待許可的線程。
        if (compareAndSetState(current, next))
            return true;
    }
}

當一個線程A調用acquire方法時,會直接嘗試獲取許可證,而不管同一時刻阻塞隊列中是否有線程也在等待許可證,如果恰好有線程C調用release方法釋放許可證,並喚醒阻塞隊列中第一個等待的線程B,此時線程A和線程B是共同競爭可用許可證,不公平性就體現在:線程A沒任何等待就和線程B一起競爭許可證了。

而在FairSync中,acquire方法核心源碼是:

protected int tryAcquireShared(int acquires) {
	//acquires參數默認爲1,表示嘗試獲取1個許可證。
    for (;;) {
    	//檢查阻塞隊列中是否有等待的線程
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        //remaining是剩餘的許可數數量。
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

和非公平策略相比,FairSync中多一個對阻塞隊列是否有等待的線程的檢查,如果沒有,就可以參與許可證的競爭;如果有,線程直接被插入到阻塞隊列尾節點並掛起,等待被喚醒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章