1. 程式人生 > >核酸檢測:讓我明白AQS原理

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

春節越來越近了,疫情也越來越嚴重,但擋不住叫練攜一家老小回老家(湖北)團聚的衝動。響應國家要求去我們做**核酸檢測**了。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611306766342-6abb6877-c070-44f0-b609-619b98618abf.png#align=left&display=inline&height=347&margin=%5Bobject%20Object%5D&name=image.png&originHeight=347&originWidth=505&size=63717&status=done&style=none&width=505) ## 獨佔鎖 --- 早上叫練帶著一家三口來到了南京市第一醫院做核酸檢測,護士小姐姐站在醫院門口攔著告訴我們人比較多,無論大人小孩,需要排隊一個個等待醫生採集唾液檢測,OK,下面我們用程式碼+圖看看我們一家三口是怎麼排隊的! ```java 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611284683327-328575c1-1adc-40f3-9b99-813456f3fcc5.png#align=left&display=inline&height=407&margin=%5Bobject%20Object%5D&name=image.png&originHeight=407&originWidth=957&size=67154&status=done&style=none&width=957) AQS全稱是**AbstractQueueSynchroniz,意為佇列同步器**,本質上是一個雙向連結串列,在AQS裡面每個執行緒都被封裝成一個Node節點,每個節點都通過尾插法新增。另外節點還有還封裝狀態資訊,比如是獨佔的還是共享的,如上面的案例就表示獨佔Node,醫生他本身是一種共享資源,在AQS內部裡面叫它state,用int型別表示,執行緒都會通過CAS的方式爭搶state。執行緒搶到鎖了,就自增,沒有搶到鎖的執行緒會阻塞等待時機被喚醒。如下圖:根據我們理解抽象出來AQS的內部結構。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611286004499-547b1ef4-8bec-48c4-b1ff-88ce2041e136.png#align=left&display=inline&height=424&margin=%5Bobject%20Object%5D&name=image.png&originHeight=424&originWidth=983&size=77571&status=done&style=none&width=983) **根據上面描述,大家看AQS不就是用Node封裝執行緒,然後把執行緒按照先來後到(****非公平鎖除外****)連線起來的雙向連結串列嘛!關於非公平鎖我之前寫《排隊打飯》案例中也通過簡單例子描述過。有興趣童鞋可以翻看下! ** ** ## 共享鎖 --- 上面我們做核酸的過程是同步執行的,叫獨佔鎖。那共享鎖是什麼意思呢?現在叫練孩子只有3歲,不能獨立完成核酸檢測,護士小姐姐感同身受,觀察叫練子是排在叫練妻後面的,就讓他們一起同時做核酸檢測。這種同時做核酸的操作,相當於同時去獲取醫生資源,我們稱之為共享鎖。下面是我們測試程式碼。 ```java 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611298738189-d5f9e42b-820b-454a-8cce-40b29fccb9fe.png#align=left&display=inline&height=433&margin=%5Bobject%20Object%5D&name=image.png&originHeight=433&originWidth=882&size=67291&status=done&style=none&width=882) ## synchronized鎖不響應中斷 --- ```java /** * @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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611299701736-370a64d7-c079-4ed1-91ab-3b73eab0b64a.png#align=left&display=inline&height=220&margin=%5Bobject%20Object%5D&name=image.png&originHeight=220&originWidth=548&size=13708&status=done&style=none&width=548) ## AQS響應中斷 --- 經常做比較知識才會融會貫通,在Lock提供lock和lockInterruptibly兩種獲取鎖的方式,其中lock方法和**synchronized**是不響應中斷的,那下面我們看看**lockInterruptibly**響應中斷是什麼意思。我們還是用核酸案例說明。 ```java 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611302174719-33066e7c-c789-4fba-b539-c54a622dfdbb.png#align=left&display=inline&height=291&margin=%5Bobject%20Object%5D&name=image.png&originHeight=291&originWidth=944&size=53011&status=done&style=none&width=944) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611302781019-8f417560-8249-4c17-8b31-4bbbece0f020.png#align=left&display=inline&height=473&margin=%5Bobject%20Object%5D&name=image.png&originHeight=473&originWidth=968&size=88503&status=done&style=none&width=968) ## 條件佇列 --- 人生或許有那麼些不如意。漫長的一個小時排隊等待終於過去了,輪到我們準備做核酸了,你說**氣不氣**,每次叫練妻出門都帶身份證,可偏偏回家這次忘記了?我們用程式碼看看叫練一家三口在做核酸的過程中到底發生了啥事情?又是怎麼處理的! ```java 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611304655228-aad9da15-45d4-42e8-b683-f915a15a2173.png#align=left&display=inline&height=274&margin=%5Bobject%20Object%5D&name=image.png&originHeight=274&originWidth=593&size=21325&status=done&style=none&width=593) 如下圖,當叫練妻先獲取鎖,發現身份證忘帶呼叫**await**方法會釋放持有的鎖,並把自己當做node節點放入條件佇列的尾部,此時條件佇列為空,所以條件佇列中只有叫練妻一個執行緒在裡面,接著護士小姐姐會將核酸醫生這個資源釋放分配給下一個等待者,也就是叫練子執行緒,同理,叫練子執行完畢釋放鎖之後會喚醒叫練執行緒,底層是用LockSupport.unpark來完成喚醒的的操作,相當於基礎系列裡的wait/notify/notifyAll等方法。當叫練執行緒執行完畢,後面沒有執行緒了,護士小姐姐呼叫singleAll方法會見條件佇列的叫練妻執行緒喚醒,並加入到AQS的尾部,等待執行。其中條件佇列是一個單向連結串列,一個AQS可以通過newCondition()對應多個條件佇列。這裡我們就不單獨用程式碼做測試了。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1611305244792-22389c84-15a8-4f1b-9cb4-39f978e92dad.png#align=left&display=inline&height=600&margin=%5Bobject%20Object%5D&name=image.png&originHeight=600&originWidth=753&size=60995&status=done&style=none&width=753) ## 總結 --- 今天我們用程式碼+圖片+故事的方式說明了AQS重要的幾個概念,整理出來希望能對你有幫助,寫的比不全,同時還有許多需要修正的地方,希望親們加以指正和點評,年前這段時間會繼續輸出實現AQS高階鎖,如:ReentrantLock,執行緒池這些概念等。最後喜歡的請點贊加關注哦。我是**叫練【公眾號】**,邊叫邊練。 **注意:本故事是自己虛構出來的,僅供大家參考理解。希望大家過年都能順利回家團聚!** **![tempImage1611306633088.gif](https://cdn.nlark.com/yuque/0/2021/gif/1897706/1611306713183-cc11c946-1455-4f12-a60e-6e84fa3d65be.gif#align=left&display=inline&height=128&margin=%5Bobject%20Object%5D&name=tempImage1611306633088.gif&originHeight=128&originWidth=128&size=20544&status=done&style=none&width