核酸檢測:讓我明白AQS原理

春節越來越近了,疫情也越來越嚴重,但擋不住叫練攜一家老小回老家(湖北)團聚的衝動。響應國家要求去我們做核酸檢測了。
image.png

獨佔鎖


早上叫練帶着一家三口來到了南京市第一醫院做核酸檢測,護士小姐姐站在醫院門口攔着告訴我們人比較多,無論大人小孩,需要排隊一個個等待醫生採集唾液檢測,OK,下面我們用代碼+圖看看我們一家三口是怎麼排隊的!

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-22 10:33
 * @description:獨佔鎖測試
 * @modified By:
 * 公眾號:叫練
 */
public class ExclusiveLockTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    //醫院
    private static class Hospital {

        private String name;

        public Hospital(String name) {
            this.name = name;
        }

        //核酸檢測排隊測試
        public void checkUp() {
            try {
                writeLock.lock();
                System.out.println(Thread.currentThread().getName()+"正在做核酸檢測");
                //核酸過程...難受...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Hospital hospital = new Hospital("南京市第一醫院");
        Thread JLWife = new Thread(()->hospital.checkUp(),"叫練妻");
        JLWife.start();
        //睡眠100毫秒是讓一家三口是有順序的排隊去檢測
        Thread.sleep(100);
        Thread JLSon = new Thread(()->hospital.checkUp(),"叫練子");
        JLSon.start();
        Thread.sleep(100);
        Thread JL = new Thread(()->hospital.checkUp(),"叫練");
        JL.start();
    }
}

如上代碼:在主線程啟動三個線程去醫院門口排隊,女士優先,叫練妻是排在最前面的,中間站的是叫練的孩子,最後就是叫練自己了。我們假設模擬了下核酸檢測一次需要3秒。代碼中我們用了獨佔鎖,獨佔鎖可以理解成醫院只有一個醫生,一個醫生同時只能為一個人做核酸,所以需要逐個排隊檢測,所以代碼執行完畢一共需要花費9秒,核酸檢測就可以全部做完。代碼邏輯還是比較簡單,和我們之前文章描述synchronized同理。核酸排隊我們用圖描述下吧!
image.png

AQS全稱是AbstractQueueSynchroniz,意為隊列同步器,本質上是一個雙向鏈表,在AQS裏面每個線程都被封裝成一個Node節點,每個節點都通過尾插法添加。另外節點還有還封裝狀態信息,比如是獨佔的還是共享的,如上面的案例就表示獨佔Node,醫生他本身是一種共享資源,在AQS內部裏面叫它state,用int類型表示,線程都會通過CAS的方式爭搶state。線程搶到鎖了,就自增,沒有搶到鎖的線程會阻塞等待時機被喚醒。如下圖:根據我們理解抽象出來AQS的內部結構。
image.png
**根據上面描述,大家看AQS不就是用Node封裝線程,然後把線程按照先來後到(非公平鎖除外)連接起來的雙向鏈表嘛!關於非公平鎖我之前寫《排隊打飯》案例中也通過簡單例子描述過。有興趣童鞋可以翻看下!
**
**

共享鎖


上面我們做核酸的過程是同步執行的,叫獨佔鎖。那共享鎖是什麼意思呢?現在叫練孩子只有3歲,不能獨立完成核酸檢測,護士小姐姐感同身受,觀察叫練子是排在叫練妻後面的,就讓他們一起同時做核酸檢測。這種同時做核酸的操作,相當於同時去獲取醫生資源,我們稱之為共享鎖。下面是我們測試代碼。

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-21 19:54
 * @description:共享鎖測試
 * @modified By:
 * 公眾號:叫練
 */
public class SharedLockTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    //醫院
    private static class Hospital {

        private String name;

        public Hospital(String name) {
            this.name = name;
        }

        //核酸檢測排隊測試
        public void checkUp() {
            try {
                readLock.lock();
                System.out.println(Thread.currentThread().getName()+"正在做核酸檢測");
                //核酸過程...難受...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Hospital hospital = new Hospital("南京市第一醫院");
        Thread JLWife = new Thread(()->hospital.checkUp(),"叫練妻");
        JLWife.start();
        //睡眠100毫秒是讓一家三口是有順序的排隊去檢測
        Thread.sleep(100);
        Thread JLSon = new Thread(()->hospital.checkUp(),"叫練子");
        JLSon.start();
        /*Thread.sleep(100);
        Thread JL = new Thread(()->hospital.checkUp(),"叫練");
        JL.start();*/
    }
    
}

上面代碼我們用ReentrantReadWriteLock.ReadLock作為讀鎖,在主線程啟動「叫練妻」和「叫練」兩個線程,本來母子倆一共需要6秒才能完成的事情,現在只需要3秒就可以做完,共享鎖好處是效率比較高。如下圖,是AQS內部某一時刻Node節點狀態。對比上圖,Node的狀態變為了共享狀態,這些節點可以同時去共享醫生資源
image.png

synchronized鎖不響應中斷


/**
 * @author :jiaolian
 * @date :Created in 2020-12-31 18:17
 * @description:sync不響應中斷
 * @modified By:
 * 公眾號:叫練
 */
public class SynchronizedInterrputedTest {

    private static class MyService {

        public synchronized void lockInterrupt() {
            try {
                System.out.println(Thread.currentThread().getName()+" 獲取到了鎖");
                while (true) {
                   //System.out.println();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        //先啟動線程A,讓線程A先擁有鎖
        Thread threadA = new Thread(()->{
            myService.lockInterrupt();
        });
        threadA.start();
        Thread.sleep(1000);
        //啟動線程B,中斷,synchronized不響應中斷!
        Thread threadB = new Thread(()->{
            myService.lockInterrupt();
        });
        threadB.start();
        Thread.sleep(1000);
        threadB.interrupt();
    }
}

如上述代碼:先啟動A線程,讓線程A先擁有鎖,睡眠1秒再啟動線程B是讓B線程處於可運行狀態,隔1秒後再中斷B線程。在控制台輸出如下:A線程獲取到了鎖,等待2秒後控制台並沒有立刻輸出報錯信息,程序一直未結束執行,說明synchronized鎖不響應中斷,需要B線程獲取鎖後才會輸出線程中斷報錯信息!
image.png

AQS響應中斷


經常做比較知識才會融會貫通,在Lock提供lock和lockInterruptibly兩種獲取鎖的方式,其中lock方法和synchronized是不響應中斷的,那下面我們看看lockInterruptibly響應中斷是什麼意思。我們還是用核酸案例說明。

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-22 15:18
 * @description:AQS響應中斷代碼測試
 * @modified By:
 * 公眾號:叫練
 */
public class AQSInterrputedTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    //醫院
    private static class Hospital {

        private String name;

        public Hospital(String name) {
            this.name = name;
        }

        //核酸檢測排隊測試
        public void checkUp() {
            try {
                writeLock.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+"正在做核酸檢測");
                //核酸過程...難受...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Hospital hospital = new Hospital("南京市第一醫院");
        Thread JLWife = new Thread(()->hospital.checkUp(),"叫練妻");
        JLWife.start();
        //睡眠100毫秒是讓一家三口是有順序的排隊去檢測
        Thread.sleep(100);
        Thread JLSon = new Thread(()->hospital.checkUp(),"叫練子");
        JLSon.start();
        Thread.sleep(100);
        Thread JL = new Thread(()->hospital.checkUp(),"叫練");
        JL.start();
        //等待1秒,中斷叫練線程
        System.out.println("護士小姐姐想和叫練私聊會!");
        Thread.sleep(1000);
        JL.interrupt();
    }
}

如上代碼:叫練一家三口採用的是獨佔鎖排隊去做核酸,叫練線程等待一秒後,護士小姐姐想和叫練私聊會!莫非小姐姐會有啥想法,於是叫練立刻中斷了這次的核酸檢測,注意是立刻中斷。控制台打印結果如下:叫練妻線程和叫練子線程都做了核酸,但叫練卻沒有做成功!因為被護士小姐姐中斷了,結果如下圖所示。所以我們能得出結論,在aqs中鎖是可以響應中斷的。現在如果將上述代碼中lockInterruptibly方法換成lock方法會發生什麼情況呢,如果換成這種方式,小姐姐再來撩我,叫練要先成功獲取鎖,也就說叫練已經到醫生旁邊準備做核酸了,小姐姐突然說有事找叫練,最終導致叫練沒有做核酸,碰上這樣的事,只能說小姐姐是存心的,小姐姐太壞了。關於lock方法不響應中斷的測試大家可以自己測試下。看看我是不是冤枉護士小姐姐了。
我們可以得出結論:在aqs中如果一個線程正在獲取鎖或者處於等待狀態,另一個線程中斷了該線程,響應中斷的意思是該線程立刻中斷,而不響應中斷的意思是該線程需要獲取鎖後再中斷。
image.png
image.png

條件隊列


人生或許有那麼些不如意。漫長的一個小時排隊等待終於過去了,輪到我們準備做核酸了,你說氣不氣,每次叫練妻出門都帶身份證,可偏偏回家這次忘記了?我們用代碼看看叫練一家三口在做核酸的過程中到底發生了啥事情?又是怎麼處理的!

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-22 16:10
 * @description:條件隊列測試
 * @modified By:
 * 公眾號:叫練
 */
public class ConditionTest {

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    //條件隊列
    private static Condition condition = writeLock.newCondition();

    //醫院
    private static class Hospital {

        private String name;

        public Hospital(String name) {
            this.name = name;
        }

        //核酸檢測排隊測試
        public void checkUp(boolean isIdCard) {
            try {
                writeLock.lock();
                validateIdCard(isIdCard);
                System.out.println(Thread.currentThread().getName()+"正在做核酸檢測");
                //核酸過程...難受...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName()+"核酸檢測完成");
            }
        }

        //校驗身份信息;
        private void validateIdCard(boolean isIdCard) {
            //如果沒有身份信息,需要等待
            if (!isIdCard) {
                try {
                    System.out.println(Thread.currentThread().getName()+"忘記帶身份證了");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        //通知所有等待的人
        public void singleAll() {
            try {
                writeLock.lock();
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }

    }


    public static void main(String[] args) throws InterruptedException {
        Hospital hospital = new Hospital("南京市第一醫院");
        Thread.currentThread().setName("護士小姐姐線程");
        Thread JLWife = new Thread(()->{
            hospital.checkUp(false);
            },"叫練妻");
        JLWife.start();
        //睡眠100毫秒是讓一家三口是有順序的排隊去檢測
        Thread.sleep(100);
        Thread JLSon = new Thread(()->hospital.checkUp(true),"叫練子");
        JLSon.start();
        Thread.sleep(100);
        Thread JL = new Thread(()->{
            hospital.checkUp(true);
        },"叫練");
        JL.start();
        //等待叫練線程執行完畢
        JL.join();
        hospital.singleAll();
    }

}

如上代碼:一家人獲取獨佔鎖需要排隊檢測,叫練妻先進去準備核酸,護士小姐姐說先要刷身份證才能進去,叫練妻突然回想起來,出門走得急身份證忘記帶了,這可咋辦,需要重新排隊嗎?叫練妻很恐慌,護士小姐姐說,要不這樣吧,你先趕緊回家拿,等叫練子,叫練先檢測完,我就趕緊安排你進去在做核酸,那樣你就不需要重新排隊了,這就是上述這段代碼的表達意思。我們看看執行結果如下圖,和我們分析的結果一致,下圖最後畫紅圈的地方叫練妻最後完成核酸檢測。下面我們看看AQS內部經歷的過程。
image.png

如下圖,當叫練妻先獲取鎖,發現身份證忘帶調用await方法會釋放持有的鎖,並把自己當做node節點放入條件隊列的尾部,此時條件隊列為空,所以條件隊列中只有叫練妻一個線程在裏面,接着護士小姐姐會將核酸醫生這個資源釋放分配給下一個等待者,也就是叫練子線程,同理,叫練子執行完畢釋放鎖之後會喚醒叫練線程,底層是用LockSupport.unpark來完成喚醒的的操作,相當於基礎系列裏的wait/notify/notifyAll等方法。當叫練線程執行完畢,後面沒有線程了,護士小姐姐調用singleAll方法會見條件隊列的叫練妻線程喚醒,並加入到AQS的尾部,等待執行。其中條件隊列是一個單向鏈表,一個AQS可以通過newCondition()對應多個條件隊列。這裡我們就不單獨用代碼做測試了。
image.png

總結


今天我們用代碼+圖片+故事的方式說明了AQS重要的幾個概念,整理出來希望能對你有幫助,寫的比不全,同時還有許多需要修正的地方,希望親們加以指正和點評,年前這段時間會繼續輸出實現AQS高級鎖,如:ReentrantLock,線程池這些概念等。最後喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。

注意:本故事是自己虛構出來的,僅供大家參考理解。希望大家過年都能順利回家團聚!
tempImage1611306633088.gif