1.1 volatile
1.1.1 JMM
? JMM 是什么
??JMM(Java 記憶體模型:Java Memory Model,簡稱 JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規則或規范,定義了程式中各個共享變數的訪問規則,即在虛擬機中將變數存盤到記憶體和從記憶體讀取變數這樣的底層細節,
??根據 JMM 的設計,系統存在一個主記憶體(Main Memory),Java 中所有實體變數都儲存在主存中,對于所有執行緒都是共享的,每個執行緒都有自己的作業記憶體(Working Memory)是私有資料區域,執行緒對變數的操作(讀取賦值等)必須在作業記憶體中進行,首先要將變數從主記憶體拷貝的自己的作業記憶體空間,然后對變數進行操作,操作完成后再將變數寫回主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的作業記憶體中存盤著主記憶體中的變數副本拷貝,不同的執行緒間無法訪問對方的作業記憶體,執行緒間的通信必須通過主記憶體來完成,

? JMM 特性
| 特性 | 說明 |
|---|---|
| 可見性 | 一個執行緒對共享變數做了修改之后,其他的執行緒立即能夠感知到該變數的修改, |
| 原子性 | 一個操作不能被打斷,要么全部執行完畢,要么不執行, |
| 有序性 | JMM 允許指令重排,但不管怎么重排,重排后的指令絕對不能改變原有的串行語意, |
1.1.2 volatile 是什么
??volatile 是 Java 提供的一種輕量級的同步機制,volatile 基本滿足了 JMM 要求,保證了可見性、禁止指令重排(有序性)但是不保證原子性,
1.1.3 可見性
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 資源類
*/
public class MyData{
// 沒有 volatile 修飾時,沒有可見性
int i = 0;
public void change() {
this.i = 100;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc Volatile 可見性
*/
public class VolatileDemo {
public static void main(String[] args) {
// 資源類
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 開始操作 ===== MyData");
try {
// 模擬操作耗時
Thread.sleep(300);
myData.change();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 操作完成 ===== MyData");
}, "volatile").start();
while (0 == myData.i) {}
System.out.println(Thread.currentThread().getName() + " 我來康康 i = " + myData.i);
}
}

??運行上述代碼可以發現,程式一致未結束,很明顯是卡在了 while 回圈,mian 執行緒一致認為 MyData 中 的變數 i 的值是 0,所以不會退出回圈,現在我們在變數 i 上加 volatile 關鍵字后再此執行,發現 mian 發現了變數 i 的修改,退出了 while 回圈,

1.1.4 原子性
? 不保證原子性
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 資源類
*/
public class MyData {
volatile int i = 0;
public void add() {
i++;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc Volatile 不保證原子性
*/
public class VolatileDemo {
public static void main(String[] args) {
// 資源類
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.add();
}
}, "add-" + i).start();
}
// mian 與 GC 執行緒
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " 我來康康 i = " + myData.i);
}
}

??運行上述代碼,輸出結果并非我們所想為 20000,這是因為 volatile 不保證原子性,會導致在執行緒執行程序中有執行緒加塞,例如,add-0 執行緒讀到 i = 1 執行 ++ 操作,add-1 執行緒也讀到 i = 1 執行 ++ 操作,正當二者要寫回主記憶體時,add-0 執行緒阻塞,add-1 執行緒將 i = 2 寫回了主記憶體,正準備通知其他執行緒 i 修改了,add-0 執行緒先一步將 i = 2 也寫回了主記憶體,就造成了丟失,
? 解決方案
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 資源類
*/
public class MyData {
volatile int i = 0;
// 不推薦使用 synchronized,高射炮打蚊子
public synchronized void add() {
i++;
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/23
* @desc 資源類
*/
public class MyData {
// 推薦使用 atomic 原子操作介面
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
1.3.5 指令重排
? 什么是指令重排
??在虛擬機層面,為了盡可能減少記憶體操作速度遠慢于 CPU 運行速度所帶來的 CPU 空置的影響,虛擬機會按照自己的一些規則將指令重排——即寫在后面的代碼在時間順序上可能會先執行,而寫在前面的代碼會后執行——以盡可能充分地利用 CPU,在硬體層面,與虛擬機層面原因類似,CPU 會將接收到的一批指令按照其規則重排序,只是硬體處理的話,每次只能在接收到的有限指令范圍內重排序,而虛擬機可以在更大層面、更多指令范圍內重排序,

??上圖便是匯編指令的執行程序,在某些指令上存在 X 的標志,X 代表中斷的含義,也就是只要有 X 的地方就會導致指令流水線技術停頓,同時也會影響后續指令的執行,可能需要經過 1 個或幾個指令周期才可能恢復正常,那為什么停頓呢?這是因為部分資料還沒準備好,如執行 ADD 指令時,需要使用到前面指令的資料 R1,R2,而此時 R2 的 MEM 操作沒有完成,即未拷貝到存盤器中,這樣加法計算就無法進行,必須等到 MEM 操作完成后才能執行,也就因此而停頓了,其他指令也是類似的情況,

??停頓會造成 CPU 性能下降,因此我們應該想辦法消除這些停頓,這時就需要使用到指令重排了,既然 ADD 指令需要等待,那我們就利用等待的時間做些別的事情,如把 LW R4,e 和 LW R5,f 移動到前面執行,畢竟 LW R4,e 和 LW R5,f 執行并沒有資料依賴關系,對他們有資料依賴關系的 SUB R6,R5,R4 指令在 R4,R5 加載完成后才執行的,所以沒有影響,重排后,所有的停頓都完美消除了,指令流水線也無需中斷了,這樣 CPU 的性能也能帶來很好的提升,這就是處理器指令重排的作用,
? 記憶體屏障
??記憶體屏障是 CPU 指令,如果你的欄位是 volatile,Java 記憶體模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令,下面是基于保守策略的 JMM 記憶體屏障插入策略:
?? 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障,
?? 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障,
?? 在每個 volatile 讀操作的前面插入一個 LoadLoad 屏障,
?? 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障,
記憶體屏障,又稱記憶體柵欄,是一組處理器指令,用于實作對記憶體操作的順序限制,

1.2 CAS
1.2.1 什么是 CAS
??CAS(Compare and swap)比較和替換是設計并發演算法時用到的一種技術,簡單來說,比較和替換是使用一個期望值和一個變數的當前值進行比較,如果當前變數的值與我們期望的值相等,就使用一個新值替換當前變數的值,AtomicInteger 類中的 compareAndSet 方法就是這種思想,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CASDemo {
public static void main(String[] args) {
// 創建初始值是 5 的原子型整數
AtomicInteger atomicInteger = new AtomicInteger(5);
// 期望當前變數值沒有人動過,依舊是 5 時,將 5 替換為 2021
System.out.println(atomicInteger.compareAndSet(5, 2021) + " == " + atomicInteger);
// 期望當前變數值沒有人動過,依舊是 5 時,將 5 替換為 2022
System.out.println(atomicInteger.compareAndSet(5, 2022) + " == " + atomicInteger);
}
}

1.2.2 CAS 原理
??在前文的 volatile 的原子性中我們使用了 AtomicInteger 類中的 getAndIncrement 方法,為什么他就能保證原子性,我們來看一下原始碼,發現最終在 Unsafe 類中使用了一個 while 回圈,首先呼叫 getIntVolatile 方法根據物件和偏移值獲取到記憶體中的資料,相當于將主存資料復制到本地作業空間,然后呼叫 compareAndSwapInt 方法來判斷期望的值與主存的值是否一致,一致則更新主存值,回傳 true 取反退出回圈,否則繼續回圈,

1.2.3 CAS 缺點
① 回圈開銷大:底層使用的是 while 回圈,極限情況可能導致回圈 N 次,性能開銷大
② 只能保證一個共享變數原子操作
③ ABA 問題:一個執行緒速度較快,將 A 改為 B 后又改為 A,其他執行緒一看還是 A 認為沒有人動過,

1.2.4 解決 ABA 問題
??ABA 問題的產生是因為有人改過而我不知道,那么改過之后記錄以下不就好了,我們都用過 Git 當我們拉取最新版本的代碼,修改了某個地方提交并推送到遠端后,覺得修改的有問題,還是原來的好,就把修改的地方恢復了再次提交并推送到遠端,這個時候另外一個人肯定知道你改過代碼,因為有版本號,同樣的 Java 也提供了攜帶版本號的原子參考型別 AtomicStampedReference<V>,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CASDemo {
public static void main(String[] args) {
// 攜帶版本號的原子參考型別
AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<>(5, 1);
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " ? 我初次獲取的版本號:" + stamp);
// 等待 B 執行緒獲取版本號
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ABA 操作
boolean b = integerAtomicStampedReference.compareAndSet(5, 100, stamp, stamp + 1);
System.out.print(Thread.currentThread().getName() + " ? 5 -> 100 為 " + b);
System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
boolean b1 = integerAtomicStampedReference.compareAndSet(100, 5, stamp + 1, stamp + 2);
System.out.print(Thread.currentThread().getName() + " ? 100 -> 5 為 " + b1);
System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
}, "A").start();
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " ? 我初次獲取的版本號:" + stamp);
// 等待 A 執行緒完成 ABA 操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = integerAtomicStampedReference.compareAndSet(5, 200, stamp, stamp + 1);
System.out.print(Thread.currentThread().getName() + " ? 5 -> 200 為 " + b);
System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
}, "B").start();
}
}

1.3 Java 中的鎖
一般我們認為鎖大體分為兩種,樂觀鎖和悲觀鎖,樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,采取在寫時先讀出當前版本號,然后加鎖操作,如果失敗則要重復讀-比較-寫的操作,Java 中的樂觀鎖基本都是通過 CAS 實作的,悲觀鎖是就是悲觀思想,即認為寫多,遇到并發寫的可能性高,每次去拿資料的時候都認為別人會修改,所以每次在讀寫資料的時候都會上鎖,這樣別人想讀寫這個資料就會 block 直到拿到鎖,
1.3.1 公平鎖/非公平鎖
??公平鎖(Fair):多個執行緒按照申請鎖的順序去獲得鎖,執行緒會直接進入佇列去排隊,永遠都是佇列的第一位才能得到鎖,優點是所有的執行緒都能得到資源,不會餓死在佇列中,缺點是吞吐量會下降很多,佇列里面除了第一個執行緒,其他的執行緒都會阻塞,CPU 喚醒阻塞執行緒的開銷會很大,
??非公平鎖(Nonfair):多個執行緒去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待佇列,如果能獲取到,就直接獲取到鎖,優點是可以減少 CPU 喚醒執行緒的開銷,整體的吞吐效率會高點,CPU 也不必取喚醒所有執行緒,會減少喚起執行緒的數量,缺點是可能導致佇列中間的執行緒一直獲取不到鎖或者長時間獲取不到鎖,導致餓死,synchronized 是非公平鎖,ReentrantLock 空參構造回傳的是非公平鎖,若是要創建公平鎖則使用 ReentrantLock 有參構造傳入 true,

1.3.2 可重入鎖(遞回鎖)
??可重入鎖,也叫做遞回鎖,它的可重入性表現在同一個執行緒可以多次獲得鎖,指的是同一執行緒有內外兩層被同一把鎖鎖住的函式,外層函式獲得鎖之后,進入內層函式時自動獲得鎖,并且不發生死鎖,ReentrantLock 和 synchronized 都是可重入鎖,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 可重入鎖
*/
public class MyText {
Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 執行外層函式");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
method2();
}
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 執行內層函式");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class ReentrantDemo {
public static void main(String[] args) {
MyText myText = new MyText();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
myText.method1();
}, ((char)(65 + i) + "").start();
}
}
}
1.3.3 自旋鎖
??自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那么那些等待競爭鎖的執行緒就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋,回圈獲取鎖),等持有鎖的執行緒釋放鎖后即可立即獲取鎖,這樣就避免用戶執行緒和內核的切換的消耗, 執行緒自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那執行緒也不能一直占用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間, 如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 自旋鎖
*/
public class MyLock {
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
while (!threadAtomicReference.compareAndSet(null, thread)){}
System.out.println(thread.getName() + " 鎖住了");
}
public void myUnLock() {
Thread thread = Thread.currentThread();
threadAtomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + " 開鎖了");
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class SpinLockDemo {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(() -> {
lock.myLock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.myUnLock();
}, "A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.myLock();
lock.myUnLock();
}, "B").start();
}
}
1.3.4 獨占鎖(寫鎖)/共享鎖(讀鎖)
??獨占鎖:獨占鎖也叫排他鎖,是指該鎖一次只能被一個執行緒所持有,如果執行緒 T 對資料 A 加上排他鎖后,則其他執行緒不能再對 A 加任何型別的鎖,獲得排它鎖的執行緒即能讀資料又能修改資料,ReentrantLock 和 synchronized 都是獨占鎖
??共享鎖:共享鎖是指該鎖可被多個執行緒所持有,如果執行緒 T 對資料 A 加上共享鎖后,則其他執行緒只能對 A 再加共享鎖,不能加排它鎖,獲得共享鎖的執行緒只能讀資料,不能修改資料,ReentrantReadWriteLock 讀鎖是共享鎖,寫鎖是獨占鎖,讀鎖的共享可以保證并發讀是高效的,讀寫,寫讀,寫寫是互斥的
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc 讀寫鎖
*/
public class MyCache {
private volatile Map<String, String> map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void put(String key, String value) {
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在寫入:" + key);
Thread.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 寫入完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
public void get(String key) {
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在讀取:" + key);
Thread.sleep(300);
map.get(key);
System.out.println(Thread.currentThread().getName() + " 讀取完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc //TODO
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", "");
}, ((char)(65 + i) + "")).start();
}
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI + "");
}, ((char)(65 + i) + "")).start();
}
}
}
為什么要加讀鎖?
??讀鎖自然也是為了避免原子性問題,比如一個 long 型引數的寫操作并不是原子性的,如果允許同時讀和寫,那讀到的數很可能是就是寫操作的中間狀態,比如剛寫完前 32位,就被讀到了,
1.4 JUC 并發工具類
1.4.1 CountDownLatch
??CountDownLatch 這個類使一個執行緒等待其他執行緒各自執行完畢后再執行,內部是通過一個計數器來實作的,計數器的初始值是執行緒的數量,每當一個執行緒執行完畢后,計數器的值就 -1,當計數器的值為 0 時,表示所有執行緒都執行完畢,然后等待的執行緒就可以恢復作業了,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CountDownLatch
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 吃完了");
countDownLatch.countDown();
}, ((char)(65 + i) + "")).start();
}
try {
// 等待執行緒執行完畢
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 收桌子了");
}
}

1.4.2 CyclicBarrier
??CyclicBarrier 的字面意思是可回圈使用(Cyclic)的屏障(Barrier),它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續干活,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc CAS
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召喚神龍"));
for (int i = 0; i < 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 收集到了龍珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, ((char)(65 + i) + "")).start();
}
}
}

1.4.3 Semaphore
??Semaphore 是計數信號量,Semaphore 管理一系列許可,每個 acquire 方法阻塞,直到有一個許可證可以獲得然后拿走一個許可證;每個 release 方法增加一個許可,這可能會釋放一個阻塞的 acquire 方法,然而,其實并沒有實際的許可這個物件,Semaphore 只是維持了一個可獲得許可證的數量,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc Semaphore
*/
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 搶到了車位");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 離開了車位");
}
}, ((char)(65 + i) + "")).start();
}
}
}

1.4.4 Exchanger
??Exchanger 是 JDK 1.5 開始提供的一個用于兩個作業執行緒之間交換資料的封裝工具類,簡單說就是一個執行緒在完成一定的事務后想與另一個執行緒交換資料,則第一個先拿出資料的執行緒會一直等待第二個執行緒,直到第二個執行緒拿著資料到來時才能彼此交換對應資料,其定義為 Exchanger<V> 泛型型別,其中 V 表示可交換的資料型別,
/**
* @author Demo_Null
* @version 1.0
* @date 2021/2/24
* @desc Exchanger
*/
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> stringExchanger = new Exchanger<>();
new Thread(() -> {
try {
String data = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + " 交換前資料:" + data);
String exchange = stringExchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 交換后資料:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
String data = Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + " 交換前資料:" + data);
String exchange = stringExchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + " 交換后資料:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}

1.4.5 工具類匯總

1.5 AQS
1.5.1 什么是 AQS
??AbstractQueuedSynchronizer 類如其名,抽象的佇列式的同步器,AQS 提供了原子式管理同步狀態、阻塞和喚醒執行緒功能以及佇列模型的簡單框架,Java 中的大部分同步類(Lock、Semaphore、ReentrantLock 等)都是基于 AQS 實作的,AQS 是用來構建鎖或者其它同步器組件的重量級基礎框架及整個 JUC 體系的基石,使用一個 volatile 的 int 型別的成員變數來表示同步狀態,通過內置的 FIFO 佇列來完成資源獲取的排隊作業,將每條要去搶占資源的執行緒封裝成 一個 Node 節點來實作鎖的分配,通過 CAS 完成對 State 值的修改,
1.5.2 AQS 能干嘛
??搶到資源的執行緒可以直接執行業務邏輯,搶占不到資源的執行緒的必然要去排隊等候,AQS 的作業就是將排隊等候安排的明明白白的,AQS 內部維護了一個 CLH 佇列來管理鎖,執行緒會首先嘗試獲取鎖,如果失敗就將當前執行緒及等待狀態等資訊包裝成一個 node 節點加入到同步佇列 sync queue里,當前節點為 head 的直接后繼節點時就會嘗試獲取鎖,如果失敗就會阻塞自己直到自己被喚醒,而當持有鎖的執行緒釋放鎖的時候,會喚醒佇列中的后繼執行緒,

CLH(Craig,Landin,and Hagersten)佇列是一個虛擬的雙向佇列,虛擬的雙向佇列即不存在佇列實體,僅存在結點之間的關聯關系,
1.5.3 AQS 原始碼決議(ReentrantLock 為例)

??最開始,一切準備就緒,但是沒有任何執行緒進來,好比銀行剛開門沒有任何人開辦理業務,

??第一個執行緒 ThreadA 開始呼叫 lock 方法搶占鎖,首先判斷當前 state 是否是 0 空閑狀態,若是則將其設定為 1,然后將當前執行執行緒設定為 ThreadA,就好比第一個顧客進入銀行后獨占唯一的一個視窗開始辦理業務,


??第二個執行緒 ThreadB 也開始呼叫 lock 方法搶占鎖,發現 state 為 1,有人占了,然后就執行 acquire 方法,acquire 方法呼叫 tryAcquire 方法,tryAcquire 方法又呼叫 nonfairTryAcquire 方法,在 nonfairTryAcquire 方法中判斷當前 state 是否為 0,不為零在判斷當前執行緒是否是正在執行的執行緒,此處由于 ThreadA 仍在執行,所以回傳 false,

??然后呼叫 addWaiter 方法,在 addWaiter 方法呼叫 enq 方法,很明顯這個方法里面是一個自旋,第一次由于尾指標 tail 是指向 null 的,所以添加一個空的節點,該節點被稱為哨兵節點,并將 tail 指向哨兵節點;第二次 tail 非空,則將 tail 指向真正的 ThreadB node,并將哨兵節點的 next 也指向該 node,緊接著執行 acquireQueued 方法,該方法里面又是一個自旋,自旋時當前為哨兵節點后第一個節點時會再次嘗試搶占鎖,未搶到會呼叫 shouldParkAfterFailedAcquire 方法,將哨兵節點的 waitStatus 設為 -1 后進行第二次自旋,第二次自旋 shouldParkAfterFailedAcquire 回傳 true,開始執行 parkAndCheckInterrupt 方法,該方法讓 ThreadB 阻塞,


??第三個執行緒 ThreadC 也開始呼叫 lock 方法搶占鎖,同理將其加入到佇列等待,ThreadB、ThreadC 的這種操作類似于,第二個顧客進入銀行,看到第一個顧客在辦理業務,就走到了候客區,然后再看了一眼第一個顧客,發現還沒有辦理完,然后坐下了,

??ThreadA 執行完畢后,呼叫 unlock 釋放鎖,在 unlock 方法中呼叫 release 方法,在 release 方法中呼叫 tryRelease 方法,tryRelease 方法將 state 設為 0 并將當前執行執行緒設定為 null,然后呼叫 unparkSuccessor 方法,在該方法中將 ThreadB 的 waitStatus 設為 0,然后喚醒 ThreadB,

??ThreadB 被喚醒后,回傳 false,繼續自旋,這個時候由于沒有執行緒占用,ThreadB 直接獲取到了鎖,然后將當前 ThreadB 節點修改為哨兵節點,原有哨兵節點等待 GC 回收,


轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/265858.html
標籤:其他
上一篇:【Java自頂向下】ConcurrentHashMap面試題(2021最新版)
下一篇:一文帶你識別移動端主流加固的方案
