春節越來越近了,疫情也越來越嚴重,但擋不住叫練攜一家老小回老家(湖北)團聚的沖動。響應國家要求,我們去做核酸檢測了。
獨占鎖
早上叫練帶着一家三口來到了南京市第一醫院做核酸檢測,護士小姐姐站在醫院門口攔着告訴我們人比較多,無論大人小孩,需要排隊一個個等待醫生采集唾液檢測,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同理。核酸排隊我們用圖描述下吧!
AQS全稱是AbstractQueueSynchroniz,意為隊列同步器,本質上是一個雙向鏈表,在AQS里面每個線程都被封裝成一個Node節點,每個節點都通過尾插法添加。另外節點還有還封裝狀態信息,比如是獨占的還是共享的,如上面的案例就表示獨占Node,醫生他本身是一種共享資源,在AQS內部里面叫它state,用int類型表示,線程都會通過CAS的方式爭搶state。線程搶到鎖了,就自增,沒有搶到鎖的線程會阻塞等待時機被喚醒。如下圖:根據我們理解抽象出來AQS的內部結構。
**根據上面描述,大家看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的狀態變為了共享狀態,這些節點可以同時去共享醫生資源!
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線程獲取鎖后才會輸出線程中斷報錯信息!
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中如果一個線程正在獲取鎖或者處於等待狀態,另一個線程中斷了該線程,響應中斷的意思是該線程立刻中斷,而不響應中斷的意思是該線程需要獲取鎖后再中斷。
條件隊列
人生或許有那么些不如意。漫長的一個小時排隊等待終於過去了,輪到我們准備做核酸了,你說氣不氣,每次叫練妻出門都帶身份證,可偏偏回家這次忘記了?我們用代碼看看叫練一家三口在做核酸的過程中到底發生了啥事情?又是怎么處理的!
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內部經歷的過程。
如下圖,當叫練妻先獲取鎖,發現身份證忘帶調用await方法會釋放持有的鎖,並把自己當做node節點放入條件隊列的尾部,此時條件隊列為空,所以條件隊列中只有叫練妻一個線程在里面,接着護士小姐姐會將核酸醫生這個資源釋放分配給下一個等待者,也就是叫練子線程,同理,叫練子執行完畢釋放鎖之后會喚醒叫練線程,底層是用LockSupport.unpark來完成喚醒的的操作,相當於基礎系列里的wait/notify/notifyAll等方法。當叫練線程執行完畢,后面沒有線程了,護士小姐姐調用singleAll方法會見條件隊列的叫練妻線程喚醒,並加入到AQS的尾部,等待執行。其中條件隊列是一個單向鏈表,一個AQS可以通過newCondition()對應多個條件隊列。這里我們就不單獨用代碼做測試了。
總結
今天我們用代碼+圖片+故事的方式說明了AQS重要的幾個概念,整理出來希望能對你有幫助,寫的比不全,同時還有許多需要修正的地方,希望親們加以指正和點評,年前這段時間會繼續輸出實現AQS高級鎖,如:ReentrantLock,線程池這些概念等。最后喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。
注意:本故事是自己虛構出來的,僅供大家參考理解。希望大家過年都能順利回家團聚!