美團一面,面試官讓介紹AQS原理並手寫一個同步器,直接涼了

JavaBuild發表於2024-04-10

寫在開頭

今天在牛客上看到了一個帖子,一個網友吐槽美團一面上來就讓手撕同步器,沒整出來,結果面試直接涼涼。
image

就此聯想到一週前寫的一篇關於AQS知識點解析的博文,當時也曾埋下伏筆說後面會根據AQS的原理實現一個自定義的同步器,那今天就來把這個坑給填上哈。

常用的AQS架構同步器類

自定義同步器實現步驟

在上一篇文章中我們就已經提過了AQS是基於 模版方法模式 的,我們基於此的自定義同步器設計一般需要如下兩步:

1. 使用者繼承 AbstractQueuedSynchronizer 並重寫指定的方法;
2. 將 AQS 組合在自定義同步元件的實現中,並呼叫其模板方法,而這些模板方法會呼叫使用者重寫的方法。

在模版方法模式下,有個很重要的東西,那就是“鉤子方法”,這是一種抽象類中的方法,一般使用 protected 關鍵字修飾,可以給與預設實現,空方法居多,其內容邏輯由子類實現,為什麼不適用抽象方法呢?因為,抽象方法需要子類全部實現,增加大量程式碼冗餘!

Ok,有了這層理論知識,我們去看看Java中根據AQS實現的同步工具類有哪些吧

Semaphore(訊號量)

在前面我們講過的synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源,而Semaphore(訊號量)可以用來控制同時訪問特定資源的執行緒數量,它並不能保證執行緒安全。

我們下面來看一個關於Semaphore的使用示例:

【程式碼示例1】

public class Test {
    private final Semaphore semaphore;

    /**
     * 構造方法初始化訊號量
     * @param limit
     */
    public Test(int limit) {
        this.semaphore = new Semaphore(limit);
    }

    public void useResource() {
        try {
            semaphore.acquire();
            // 使用資源
            System.out.println("資源use:" + Thread.currentThread().getName());
            Thread.sleep(1000); // 模擬資源使用時間
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
            System.out.println("資源release:" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        // 限制3個執行緒同時訪問資源
        Test pool = new Test(3);

        for (int i = 0; i < 4; i++) {
            new Thread(pool::useResource).start();
        }
    }
}

輸出:

資源use:Thread-1
資源use:Thread-0
資源use:Thread-2
資源release:Thread-0
資源release:Thread-1
資源release:Thread-2
資源use:Thread-3
資源release:Thread-3

由此結果可看出,我們成功的將同時訪問共享資源的執行緒數限制在了不超過3個的級別,這裡面涉及到了Semaphore的兩個主要方法:acquire()和release()

① acquire():獲取許可
跟進這個方法後,我們會發現其內部呼叫了AQS的一個final 方法acquireSharedInterruptibly(),這個方法中又呼叫了tryAcquireShared(arg)放,作為AQS中的鉤子方法,這個方法的實現在Semaphore的兩個靜態內部類 FairSync(公平模式)NonfairSync(非公平模式) 中。
image

② release():釋放許可
同樣跟入這個方法,裡面用了AQS的releaseShared(),而在這個方法內也毫無疑問的用了tryReleaseShared(int arg)這個鉤子方法,原理同上,不再冗釋。

【補充】
此外,在Semaphore中還有一個Sync的內部類,提供nonfairTryAcquireShared()自旋獲取資源,以及tryReleaseShared(int releases),共享方式嘗試釋放資源。

除了Semaphore(訊號量)外,基於AQS實現的還有CountDownLatch (倒數計時器)、CyclicBarrier(迴圈柵欄),本來想在一篇文章中講完的,但感覺篇幅上會非常長,遂放棄,後面分篇學習吧。

手寫一個同步器!

好了,有了上面的一系列學習,我們現在來手撕一個自定義的同步器吧,原理都一樣滴,開始前,先貼上AQS中的幾個鉤子方法,防止待會忘記,哈哈!

【鉤子方法】

//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
protected boolean tryAcquire(int)
//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryRelease(int)
//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
protected int tryAcquireShared(int)
//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryReleaseShared(int)
//該執行緒是否正在獨佔資源。只有用到condition才需要去實現它。
protected boolean isHeldExclusively()

寫一個基於AQS的互斥鎖,統一時刻只允許一個執行緒獲取資源。

步驟一:

首先,我們在第一步,我們定義一個互斥鎖類OnlySyncByAQS,在類中我們同樣寫一個靜態內部類去繼承AbstractQueuedSynchronizer,在內部類中,我們重寫AQS的tryAcquire方法,獨佔方式,嘗試獲取資源;重寫tryRelease()嘗試釋放資源,這倆為主要方法!

然後我們再進一步封裝成lock()與unlock()的上鎖與解鎖方法,並在裡面透過模版方法模式,去呼叫AQS中的acquire()和release(),從而去調到我們對模版方法的實現。

【程式碼示例2】

public class OnlySyncByAQS {

    private final Sync sync = new Sync();

    /**
     * 獲取許可,給資源上鎖
     */
    public void lock() {
        sync.acquire(1);
    }

    /**
     * 釋放許可,解鎖
     */
    public void unlock() {
        sync.release(1);
    }

    /**
     * 判斷是否獨佔
     * @return
     */
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    /**
     * 靜態內部類,繼承AQS,重寫鉤子方法
     */
    private static class Sync extends AbstractQueuedSynchronizer {

        /**
         * 重寫AQS的tryAcquire方法,獨佔方式,嘗試獲取資源。
         */
        @Override
        protected boolean tryAcquire(int arg) {
            //CAS 嘗試更改狀態
            if (compareAndSetState(0, 1)) {
                //獨佔模式下,設定鎖的持有者為當前執行緒,來自於AOS
                setExclusiveOwnerThread(Thread.currentThread());
                System.out.println(Thread.currentThread().getName()+"獲取鎖成功");
                return true;
            }
            System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
            return false;
        }

        /**
         * 獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
         * @param arg
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            //置空鎖的持有者
            setExclusiveOwnerThread(null);
            //改狀態為0,未鎖定狀態
            setState(0);
            System.out.println(Thread.currentThread().getName()+"釋放鎖成功!");
            return true;
        }

        /**
         * 判斷該執行緒是否正在獨佔資源,返回state=1
         * @return
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

}

步驟二:

第二步,我們寫一個測試類去呼叫這個自定義的互斥鎖。

【程式碼示例2】

public class Test {

    private OnlySyncByAQS onlySyncByAQS = new OnlySyncByAQS();

    public void use(){
        onlySyncByAQS.lock();
        try {
            //休眠1秒獲取使用共享資源
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            onlySyncByAQS.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        //多執行緒競爭資源,每次僅一個執行緒拿到鎖
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                test.use();
            }).start();
        }
    }
}

輸出:

Thread-0獲取鎖成功
Thread-1獲取鎖失敗
Thread-2獲取鎖失敗
Thread-1獲取鎖失敗
Thread-1獲取鎖失敗
Thread-0釋放鎖成功!
Thread-1獲取鎖成功
Thread-1釋放鎖成功!
Thread-2獲取鎖成功
Thread-2釋放鎖成功!

由輸出結果可以看出作為互斥鎖,每次僅一個執行緒可以獲取到鎖資源,其他執行緒會不斷嘗試獲取並失敗,直至該執行緒釋放鎖資源!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章