Semaphore原理淺析和相關面試題分析

本文首發在個人公衆號:HelloWorldEE,歡迎關注。

本篇文章的來源是這樣,有一天,我一同學面試某公司回來,和我分享其被問的相關面試題。其中就有一道關於Semaphore的面試題,個人覺得比較經典,分享出來供大家參考。

具體同學和面試官的對話還原出來是這樣。

面試官:現在有一個方法task,希望只能被10個線程調用,利用Java相關類,應該如何來實現?

同學:使用Java中的Semaphore類來實現,當一個線程調用方法task之前先從Semaphore申請令牌,如果申請到了,則調用task方法,調用完之後釋放令牌供其他的線程使用,如果沒有,則阻塞等待。

面試官:Semaphore中申請令牌、釋放令牌的方法叫什麼?

同學:acquire、release方法

面試官:semaphore初始化有10個令牌,11個線程同時各調用1次acquire方法,會發生什麼?

同學:拿不到令牌的線程阻塞,不會繼續往下運行。

面試官.semaphore初始化有10個令牌,一個線程重複調用11次acquire方法,會發生什麼?

同學:稍微有點蒙,因爲考慮到了鎖的重入問題,不知道令牌會不會和鎖一樣,是可以重入的。注:筆者留給大家思考,後面會給出答案。

面試官:semaphore初始化有1個令牌,1個線程調用一次acquire方法,然後調用兩次release方法,之後另外一個線程調用acquire(2)方法,此線程能夠獲取到足夠的令牌並繼續運行嗎?

同學:額,比較蒙,一個線程獲取一個令牌然後釋放兩個令牌,Semaphore會允許嗎?同學回答應該不允許拿一個令牌然後釋放多個令牌。注:筆者留給大家思考,後面會給出答案。

面試官.semaphore初始化有2個令牌,一個線程調用1次release方法,然後一次性獲取3個令牌,會獲取到嗎?

同學:額,繼續蒙。。。心裏想:不調用acquire方法來獲取令牌,直接調用release方法來釋放令牌,semaphore允許嗎?

到這裏,關於Semaphore的面試題就結束了,一問接一問,一環接一環,確實如果對Semaphore這個類沒有特別細緻的研究,回答起來是比較困難的。除了這個問題,還有其他的問題也相當經典,在後續的文章中,筆者會一一分析出來給大家分享,這樣一場精彩的面試,爲同學和面試官點贊。

下面回到Semaphore類,關於J.C.U包下Semaphore這個類,作爲Java程序員,應該或許都會聽說過和使用過。

這個類的作用:常常被用來控制訪問速率。

例如:對於某一種資源,我們希望其最多被N個線程同時訪問。

真實的場景:庫存秒殺限流。

具體代碼實現如下:

public class TestSemaphore {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        final Semaphore semaphore = new Semaphore(2);
        for (int index = 0; index < 10; index++) {
            final int taskId = index;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //1.獲取執行令牌,如果獲取不到,則阻塞
                        semaphore.acquire();
                        //2.執行任務
                        task();
                        //3.釋放令牌
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executorService.shutdown();

    }
    public static void task() {
        try {
            System.out.println("task開始執行");
            Thread.sleep(1000);
            System.out.println("task執行結束");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

看了上面的例子,對於Semaphore的使用,是比較簡單的。其大致過程如下:

1.調用acquire來獲取令牌,如果獲取到,則繼續運行,運行完成之後,調用release方法釋放令牌供後面的線程使用。

2.如果獲取不到,則等待,直到有令牌空閒出來,其纔會被喚醒然後獲取令牌之後繼續運行。

通過上面的示例,怎麼使用,我們應該都清楚了。那麼對於acquire、release方法的實現原理是怎麼樣的呢?如果有興趣,你可以去看一眼這個類的源碼,比較簡單,就是藉助了一個volatile的變量state,然後當調用acquire方法是,就是對state進行減一操作,由於state–操作不是原子性操作,因爲爲保證原子性,採用的是cas來完成。當調用release,就是對state進行加一操作,也是採用的cas來完成。

下面是acquire、release方法的部分核心代碼。

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


  protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

看完相關的實現原理,簡單來說:semaphore就是一個計算器。更具體、詳細的源碼分析可以參考相應的文章。

回到文章開頭的面試題

問題1.semaphore初始化有10個令牌,11個線程同時各調用1次acquire方法,會發生什麼?

答案:拿不到令牌的線程阻塞,不會繼續往下運行。

問題2.semaphore初始化有10個令牌,一個線程重複調用11次acquire方法,會發生什麼?

答案:線程阻塞,不會繼續往下運行。可能你會考慮類似於鎖的重入的問題,很好,但是,令牌沒有重入的概念。你只要調用一次acquire方法,就需要有一個令牌才能繼續運行。

問題3.semaphore初始化有1個令牌,1個線程調用一次acquire方法,然後調用兩次release方法,之後另外一個線程調用acquire(2)方法,此線程能夠獲取到足夠的令牌並繼續運行嗎?

答案:能,原因是release方法會添加令牌,並不會以初始化的大小爲準。

問題4.semaphore初始化有2個令牌,一個線程調用1次release方法,然後一次性獲取3個令牌,會獲取到嗎?

答案:能,原因是release會添加令牌,並不會以初始化的大小爲準。Semaphore中release方法的調用並沒有限制要在acquire後調用。

具體示例如下,如果不相信的話,可以運行一下下面的demo,在做實驗之前,筆者也認爲應該是不允許的。。

public class TestSemaphore2 {
    public static void main(String[] args) {
        int permitsNum = 2;
        final Semaphore semaphore = new Semaphore(permitsNum);
        try {
            System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
            semaphore.release();
            System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
        }catch (Exception e) {

        }
    }
}

到這裏,關於Semaphore的分析,以及常見面試題的分析就結束了,希望對大家有一定的收穫。

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