java多執行緒系列:Semaphore和Exchanger

雲梟發表於2018-09-04

本篇文章將介紹Semaphore和Exchanger這兩個併發工具類。

Semaphore

訊號量(英語:Semaphore)又稱為訊號標,是一個同步物件,用於保持在0至指定最大值之間的一個計數值。當執行緒完成一次對該semaphore物件的等待(wait)時,該計數值減一;當執行緒完成一次對semaphore物件的釋放(release)時,計數值加一。當計數值為0,則執行緒等待該semaphore物件不再能成功直至該semaphore物件變成signaled狀態。semaphore物件的計數值大於0,為signaled狀態;計數值等於0,為nonsignaled狀態.

semaphore物件適用於控制一個僅支援有限個使用者的共享資源,是一種不需要使用忙碌等待(busy waiting)的方法。 ----取自維基百科

Semaphore思想在分散式中也有應用,分散式限流就是典型的案例。現在舉個小例子來使用Semaphore

案例

在等公交時,遇到人多的時候經常需要排隊或者擠進去。

java多執行緒系列:Semaphore和Exchanger

解決方案

利用Semaphore初始化5個許可,每次只能有5個玩家進入,當有玩家退出時,其他玩家才能進入。

先介紹下Semaphore的建構函式和一些方法吧。

Semaphore建構函式

public Semaphore(int permits);
public Semaphore(int permits, boolean fair);
複製程式碼

第一個引數permits表示初始化的許可數量,第二個參數列示是否是公平的。

使用Semaphore(int permits)建構函式時,預設使用非公平的

Semaphore常用方法

public void acquire();
public void release();
複製程式碼

acquire方法取得許可,release方法表示釋放許可。

注:如果多次呼叫release方法,會增加許可。例如,初始化許可為0,這時呼叫了兩個release方法,Semaphore的許可便會變成2

這兩個是最常用的方法,其他的還有acquire相關的方法tryAcquire和acquireUninterruptibly這裡就不介紹了。

程式碼

玩家類

定義一個實現Runnable介面的玩家類

public class Player implements Runnable{

    private String playerName;
    private Semaphore semaphore;

    public Player(String playerName, Semaphore semaphore) {
        this.playerName = playerName;
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();

            System.out.println(playerName+"進入,時間:"+LocalTime.now());
            Thread.sleep((long) (3000 * Math.random()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(playerName+"退出");
            semaphore.release();
        }
    }
}
複製程式碼

通過建構函式Player傳入玩家名稱和Semaphore物件,run方法中先呼叫acquire方法取得許可,然後睡眠隨機時間,最後在finally中呼叫release方法釋放許可。

測試類

先來使用非公平的看看效果,使用非公平的就好比平時的擠公交,誰先在車門口誰先進。如下圖(來源於網路)

java多執行緒系列:Semaphore和Exchanger

現在來看看測試程式碼

public static void main(String[] args) throws IOException {
    Semaphore semaphore = new Semaphore(5);
    //Semaphore semaphore = new Semaphore(5,true);

    ExecutorService service = Executors.newCachedThreadPool();
    //模擬100個玩家排隊
    for (int i = 0; i < 100; i++) {
        service.submit(new Player("玩家"+i,semaphore));
    }
    //關閉執行緒池
    service.shutdown();

    //判斷執行緒池是否中斷,沒有則迴圈檢視當前排隊總人數
    while (!service.isTerminated()){
        System.out.println("當前排隊總人數:"+semaphore.getQueueLength());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

如果要切換成公平方式只需將上面初始化Semaphore改為下面的程式碼即可

Semaphore semaphore = new Semaphore(5,true);
複製程式碼

Exchanger

Exchanger主要用於執行緒間的資料交換。 它提供了一個同步點在這個同步點,兩個執行緒可以交換資料

這裡寫了個兩個執行緒互相交換資料的簡單例子,下面ExchangerRunnable在run方法中呼叫exchange方法將自己的資料傳過去。

public class ExchangerRunnable implements Runnable {
    private Object data;
    private String name;
    private Exchanger exchanger;

    public ExchangerRunnable(String name, Exchanger exchanger, Object data) {
        this.exchanger = exchanger;
        this.name = name;
        this.data = data;
    }

    public void run() {
        try {
            Object previous = this.data;

            this.data = this.exchanger.exchange(previous);

            System.out.println("名稱:" + name + " 之前資料:" + previous + " ,交換之後資料:" + this.data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

接下來看看測試程式碼


public class Case {

    private static final Exchanger exchanger = new Exchanger();

    private static ExecutorService service = Executors.newFixedThreadPool(2);

    public static void main(String[] args) {

        service.submit(new ExchangerRunnable("1", exchanger, "A"));
        service.submit(new ExchangerRunnable("2", exchanger, "B"));

        service.shutdown();

    }
}
複製程式碼

定義了只包含兩個執行緒的執行緒池,然後建立提交兩個ExchangerRunnable的類

  1. 執行緒名稱為1的原始資料時A
  2. 執行緒名稱為2的原始資料時B

執行測試程式碼,會得到如下結果

名稱:2 之前資料:B ,交換之後資料:A
名稱:1 之前資料:A ,交換之後資料:B
複製程式碼

案例原始碼地址:github.com/rainbowda/l…

歡迎fork、Star、Issue等,謝謝

相關文章