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

春節愈來愈近了,疫情也愈來愈嚴重,但擋不住叫練攜一家老少回老家(湖北)團聚的衝動。響應國家要求咱們去作核酸檢測了。java

image.png


獨佔鎖


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

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同理。核酸排隊咱們用圖描述下吧!ide

image.png


AQS全稱是AbstractQueueSynchroniz,意爲隊列同步器,本質上是一個雙向鏈表,在AQS裏面每一個線程都被封裝成一個Node節點,每一個節點都經過尾插法添加。另外節點還有還封裝狀態信息,好比是獨佔的仍是共享的,如上面的案例就表示獨佔Node,醫生他自己是一種共享資源,在AQS內部裏面叫它state,用int類型表示,線程都會經過CAS的方式爭搶state。線程搶到鎖了,就自增,沒有搶到鎖的線程會阻塞等待時機被喚醒。以下圖:根據咱們理解抽象出來AQS的內部結構。測試

image.png

根據上面描述,你們看AQS不就是用Node封裝線程,而後把線程按照先來後到(非公平鎖除外)鏈接起來的雙向鏈表嘛!關於非公平鎖我以前寫《排隊打飯》案例中也經過簡單例子描述過。有興趣童鞋能夠翻看下!
this



共享鎖


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

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線程獲取鎖後纔會輸出線程中斷報錯信息!3d

image.png


AQS響應中斷


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

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

相關文章
相關標籤/搜索