1. 程式人生 > >Semaphore原理、實戰和原始碼分析

Semaphore原理、實戰和原始碼分析

一 工作原理

    Semaphore(計數訊號量),內部維護一組許可證,通過acquire方法獲取許可證,如果獲取不到,則阻塞;

通過release釋放許可,即新增許可證。

    許可證其實是Semaphore中維護的一個volatile整型state變數,初始化的時候定義一個數量,獲取時減少,釋放時增加,

一直都是在操作state。

    Semaphore內部基於AQS(同步框架)實現了公平或分公平兩種方式獲取資源。

    Semaphore主要用於限制執行緒數量、一些公共資源的訪問。

   下面通過例項體驗Semaphore的含義,然後在從原始碼角度分析(jdk1.8)Semaphore的實現原理。

二 實戰

下面舉一個獨特的例子,吃飯的時候千萬別看,看完怕吃不下飯。

我們公司每層樓都有一個衛生間,每個衛生間有5個大號坑,真心不夠用啊!

衛生間是公共資源,這裡用Semaphore來模擬現實的排隊上廁所這件事情。

1)通過acquire獲取鎖

衛生間有5個固定的坑,通過acquire來獲取坑,獲取到就用,如果沒有獲取到就阻塞,就憋著,排隊等待,

總不能踹開門把人家拽出來吧,我們都是文明人。

2)通過release釋放鎖

上完廁所的人通過release釋放坑,資源就讓出來了,然後排隊等待的人一個一個還是通過acquire去獲取資源上廁所,

獲取不到的還是耐心等待。

3)Semphore公平或非公平獲取資源

這個例子舉到這個地方,順便把Semphore的公平或不公平獲取資源分析下。

Semaphore兩個構造器:

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

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

從程式碼可以清晰的看到,預設是非公平,fair傳true就是公平。

公平:就是一個人來了看到前面有人排隊,就老老實實的跟著排。

非公平:就是一個人來了看到一長串的人排隊,非得直接奔著坑去,拉下門,發現都有人,

然後老老實實的跟著排隊。

區別:公平就是看到有人直接老實排隊,非公平就是明明看到排隊了,非得先去試一下,然後再老老實實的排隊。

服務員,開始上菜!!!

首先定義一個廁所類,裡面通過Semaphore設定了5個坑:

package com.lanhuigu.demo9.Semaphore;

import java.util.concurrent.Semaphore;

/**
 * 衛生間有5個坑
 * @author yihonglei
 * @date 2018/9/28 19:22
 */
public class Toilet {
    /**
     * 5個固定的茅坑
     */
    private static Semaphore semaphore = new Semaphore(5, true);

    /**
     * 茅坑
     */
    static class Pit {
        private String desc;

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }
    }

    /**
     * 獲取一個坑
     */
    public Pit getPit() throws InterruptedException {
        semaphore.acquire();

        Pit pit = new Pit();
        pit.setDesc("獲得坑了!!!");
        return pit;
    }

    /**
     * 釋放一個坑
     */
    public Pit releasePit() {
        semaphore.release();

        Pit pit = new Pit();
        pit.setDesc("釋放了坑!!!");
        return pit;
    }

}

然後,定義一個上廁所的執行緒,表示誰上廁所:

package com.lanhuigu.demo9.Semaphore;

/**
 * 大便!!!(畫面感很強!)
 * @author yihonglei
 * @date 2018/9/29 09:40
 */
public class ShiftThread implements Runnable {
    private Toilet toilet;
    private Integer num;

    public ShiftThread(Toilet toilet, Integer num) {
        this.toilet = toilet;
        this.num = num;
    }

    @Override
    public void run() {
        try {
            // 獲得坑
            Toilet.Pit pitAcquire = toilet.getPit();
            System.out.println("序號:" + num + pitAcquire.getDesc());
            // 解決大號
            Thread.sleep(9000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放坑
            Toilet.Pit pitRelealse = toilet.releasePit();
            System.out.println("序號:" + num + pitRelealse.getDesc());
        }
    }
}

最後,模擬下上廁所,上完,以及排隊等待的過程:

package com.lanhuigu.demo9.Semaphore;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Semaphore(計數訊號量)測試
 * @author yihonglei
 * @date 2018/9/24 23:12
 */
public class SemaphoreDemo {

    public static void main(String[] args) {
        try {
            Toilet toilet = new Toilet();
            Executor executor = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 30; i ++) {
                executor.execute(new ShiftThread(toilet, i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

程式解釋:一開始5個坑都是空的,會有5個人先後獲取了坑,其它的耐心等待,當某一個空出來的時候,

再按排隊的順序接著上,然後就是重複獲取資源和釋放資源的過程,但是最多隻能同時有5個人在坑裡。

這就是控制對公共資源的訪問,因為很多資源是有限的,有限的資源就不能過度使用,否則就亂了,

你不能一個坑裡蹲兩人吧,如果有兩個人,哪絕對不是在上廁所,而是在幹別的,幹啥就不知道了,哈哈!

三 原始碼分析

1、從構造器開始看起

1)permits為傳入的許可證數,非公平構造器;

2)permits為傳入的許可證數,fair是boolean型的,如果傳入true,則公平,否則不公平;

預設使用的是非公平構造器。

NonfairSync和FairSync原始碼:

兩者都繼承了Sync同步器,初始化時都呼叫了父類構造器,同時都有一個獲取訊號的方法,稍後再分析獲取訊號的區別。

Sync原始碼:

1)Sync為Semaphore的內部靜態類,同時繼承了AQS同步器框架,主要是再獲取或釋放訊號的時候通過

同步器的CAS演算法實現原子更新。

2)構造器呼叫了setState方法,state為Semaphore的一個成員變數,對應setState方法原始碼如下:

/**
 * The synchronization state.
 */
private volatile int state;
/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}

所以從Semaphore構造器傳進來的permits許可證數量,最後賦值到volatile變數state。

volatile是共享變數,記憶體可見,可用於執行緒間通訊,每個執行緒看到的state,一定會拿到最新的state值。

Semaphore獲取訊號或釋放訊號都是對state進行原子性減少或增加的操作。

2、acquire(獲取訊號量)

獲取訊號量預設方法原始碼:

acquire有其它的重構方法,咱們這裡分析預設獲取訊號量方法,其它的雷同。

獲取訊號量時,呼叫的是Sync的acquireSharedInterruptibly方法,預設引數為1,

Sync繼承了AQS,呼叫的其實是AQS的方法原始碼:

1)判斷當前來獲取訊號量的執行緒是否中斷,如果中斷,直接跑執行緒中斷異常。

2)tryAcauireShared是真正去獲取訊號量的方法,獲取到就返回當前訊號量剩餘數,也就是還有多少資源,

否則就返回-1。

3)如果獲取不到訊號量,tryAcauireShared方法返回-1,就會進入doAcquireSharedInterruptibly方法,

該方法會將哪些獲取不到訊號量的執行緒加入佇列裡面等待排隊。

咱們繼續看tryAcauireShared是如何處理訊號量獲取的,tryAcauireShared方法簽名:

該方法在Semaphore的靜態內部類中有兩個實現類:

先看公平的FairSync:

1)先判斷等待佇列裡面是否有正在等待獲取訊號的執行緒,如果有,獲取不到訊號量,就返回-1,外層程式碼會把

該執行緒加入等待佇列裡面,等待著獲取訊號量。

就好比上廁所,你看到有人排隊了,就不要去嘗試獲取資源了,老老實實排隊就行了,廁所肯定是滿位,要不然別人

也沒傻到有位置不用,閒得沒事在哪裡排隊玩。

這就是公平獲取訊號的邏輯,看到有排隊的,老老實實加入排隊大軍。

2)如果有資源,首先獲取可用的資源,然後減掉我們想要獲取的資源,得到剩餘的資源,也就是remaining。

判斷條件remaining<0是防止雖然沒有排隊的,但是資源剛好佔滿了,這個時候來獲取,必然沒有資源,可用為0,

remaining就是負數,直接返回負數,外層會把該執行緒加入等待佇列。

如果remaining是大於0的,則會執行後面的compareAndSetState(available,remaining)通過原子更新訊號量方式

來獲取訊號了,如果更新成功,獲取成功,返回true,這個時候返回的remaining就是大於0的,並且這個玩意就是

剩餘訊號量。所以,當能獲取訊號量時,返回的int值就是當前剩餘訊號量。

然後再看非公平NonfairSync原始碼:

有沒有發現,非公平相對於公平的程式碼只是去掉了關於等待佇列的判斷部分,非公平上來絕不判斷

佇列裡面是否有等待獲取訊號的執行緒,而是直接獲取資源,獲取不到外層處才老老實實的加入等待佇列。

就好比上廁所,大家都在排隊呢,一個哥們來了,看到排隊,不聽人說沒坑了,也不排隊,就是奔著資源去,

然後挨個門拉一遍,發現都有人,才又老老實實的去排隊。

小結下公平與非公平區別:

1)公平就是看到有執行緒等待獲取訊號了,就跟著排隊,不去試著獲取訊號;

2)非公平就是無視排隊,直接嘗試獲取訊號量,獲取不到再加入排隊大軍;

3、release(釋放訊號量)

釋放訊號量原始碼:

1)釋放訊號量。具體實現原始碼:

原始碼解析:

1)獲取當前訊號數量,也就是state變數的值。

2)當前訊號量加上要釋放的訊號量等於釋放後的訊號量。

3)next<current時拋異常,也就是釋放後的訊號量還不如釋放前多,要不是傳了負數值或者出現併發導致state為負數了。

4)通過CAS演算法原子操作訊號量state,進行訊號量釋放,恢復訊號量個數,也就是使state值增加releases個數。

2)如果tryReleaseShared嘗試釋放state成功,通過doReleaseShared進行後繼節點喚醒具體處理

茅坑讓出來了,等著的人就可以用了!等著的人也得保證先來後到,程式處理就麻煩些!

1)獲取佇列的頭節點元素,如果不為null,並且不為尾節點,說白了,就是不止一個人等待,進入判斷。

2)如果執行緒節點是需要喚醒的執行緒,則進行喚醒,獲取資源使用。

3)失敗後重試。

4)如果沒有後繼需要喚醒的節點,則退出,就相當於每人排隊上廁所了,讓出來資源就空著。

四 Semaphore總結

1、Semaphore內部維護一組訊號量,即一個volatile的整型state變數。

2、Semaphore分為公平或非公平兩種方式,獲取訊號量或釋放訊號量的本質是對state進行

原子的減少或增加操作。

3、獲取不到訊號的執行緒放在等待佇列裡面,釋放訊號的時候會喚醒後繼節點。

4、Semaphore主要用於對執行緒數量、公共資源(比如資料庫連線池)等進行數量控制。