Java基礎--AQS原理
- 1. Lock 譜系圖
- 2. Lock 接口
- 4. AbstractOwnableSynchronizer
- 5. AbstractQueuedSynchronizer
- 5.1 AQS的API
- 5.2 自定義非重入獨佔鎖
- 5.2 自定義非重入獨佔鎖的缺陷
- 5.3 自定義重入獨佔鎖
- 5.4 自定義門栓鎖
- 5.5 AQS內部線程存儲數據結構--Node
- 5.6 AQS內部行爲
- 5.6.1 tryLock
- 5.6.2 lock--acquire
- 5.6.2.1 tryAcquire
- 5.6.2.2 AddWaiter
- 5.6.2.3 enq
- 5.6.2.4 acquireQueued
- 5.6.2.5 predecessor
- 5.6.2.6 shouldParkAfterFailedAcquire
- 5.6.2.7 parkAndCheckInterrupt
- 5.6.2.8 cancelAcquire
- 5.6.2.9 unparkSuccessor
- 5.6.2.10 selfInterrupt
- 5.6.3 unlock-release
- 5.6.4 lockInterruptibly-acquireInterruptibly
- 5.6.5 tryLock(long time, TimeUtil unit)-tryAcquireNanos
- 5.6.6 await-acquireShared
- 5.6.7 releaseShared
- 6. 總結
溫馨提示:本文3.2W字左右,閱讀時間較長,慎入!
1. Lock 譜系圖
jdk對鎖的實現的類主要是2個:ReentrantLock(重入鎖),ReentrantReadWriteLock(可重入讀寫鎖)
2. Lock 接口
//嘗試獲取鎖,獲取成功則返回,否則阻塞當前線程
void lock();
//嘗試獲取鎖,線程在成功獲取鎖之前被中斷,則放棄獲取鎖,拋出異常
void lockInterruptibly() throws InterruptedException;
//嘗試獲取鎖,獲取鎖成功則返回true,否則返回false
boolean tryLock();
//嘗試獲取鎖,若在規定時間內獲取到鎖,則返回true,否則返回false,未獲取鎖之前被中斷,則拋出異常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//釋放鎖
void unlock();
//返回當前鎖的條件變量,通過條件變量可以實現類似notify和wait的功能,一個鎖可以有多個條件變量
Condition newCondition();
接下來,通過一個小例子,體驗鎖。
首先是一個併發的問題場景:
public class People {
private Long sum = 0L;
private static Long all = 0L;
public People() {
}
public Long getSum() {
return sum;
}
public void setSum(Long sum) {
this.sum = sum;
}
public Long getAll() {
return all;
}
public void setAll(Long all) {
People.all = all;
}
}
public class MyLockMain {
public static void main(String[] args) {
People people = new People();
Runnable runnable = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + thread.getId() + " start ");
for (int i = 0; i < 10000; i++) {
people.setAll(people.getAll() + 1);
people.setSum(people.getSum() + 1);
}
System.out.println(thread.getName() + thread.getId() + " end ");
};
System.out.println("main thread sum = " + people.getSum() + " , all = " + people.getAll());
ExecutorService service = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; i++) {
service.execute(runnable);
}
service.shutdown();
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
System.out.println("main interrupt exception");
}
System.out.println("main end sum = " + people.getSum() + " , all = " + people.getAll());
}
}
我們創建了一個people類,people類有兩個屬性,這兩個屬性都有get,set方法。
在主線程中,我們啓動10個線程,每個線程都對people的屬性進行增加。
我們預期的結果是這兩個屬性的值都是10W。
執行下看看結果:
這是一個併發問題,解決這個問題可以通過加鎖實現。
基於Lock接口,我們嘗試自己實現一個簡單的僞鎖:
public class MyLock implements Lock {
private volatile int value;
@Override
public void lock() {
synchronized (this) {
while (value != 0){
try {
this.wait(); // 重量級鎖 CAS自旋
} catch (InterruptedException e){
e.printStackTrace();
}
}
value = 1;
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return value == 1;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
synchronized (this) {
value = 0;
this.notifyAll();
}
}
@Override
public Condition newCondition() {
return null;
}
}
在我們自己實現的鎖中,以一個int的value的值進行標識。如果value=1標識鎖被佔用,value=0標識鎖空閒。
內部是通過synchronized實現的.
在加鎖的地方,我們通過死循環嘗試競爭鎖,如果競爭失敗,則線程掛起,線程阻塞。
在釋放鎖的地方,我們重置標誌,然後喚醒所有掛起的線程,讓被掛起的線程競爭鎖。
我們試試好不好使。
public class MyLockMain {
public static void main(String[] args) {
People people = new People();
Lock lock = new MyLock();
Runnable runnable = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + thread.getId() + " start ");
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
people.setAll(people.getAll() + 1);
people.setSum(people.getSum() + 1);
} finally {
lock.unlock();
}
}
System.out.println(thread.getName() + thread.getId() + " end ");
};
System.out.println("main thread sum = " + people.getSum() + " , all = " + people.getAll());
ExecutorService service = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; i++) {
service.execute(runnable);
}
service.shutdown();
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
System.out.println("main interrupt exception");
}
System.out.println("main end sum = " + people.getSum() + " , all = " + people.getAll());
}
}
雖然我們實際還是使用synchronized實現的同步,但是,通過這個小例子,可以體驗鎖是什麼。
4. AbstractOwnableSynchronizer
抽象線程持有類,簡稱AOS.
AOS的代碼邏輯很簡單:
AOS是抽象類,有一個內存可見的非持久化的線程屬性,以及線程屬性對應的get,set方法。
如果鎖是獨佔鎖,那麼這個類中的線程就是鎖持有者的線程。換個角度理解就是,AOS中的線程佔有鎖。
5. AbstractQueuedSynchronizer
抽象隊列同步器,簡稱AQS。
AQS繼承了AOS.
AQS內部比較複雜,AQS內部有一個Sync的抽象類,Sync抽象類有兩個實現,NonfairSync和FairSync。
其中NonfaireSync是非公平獲取鎖。
FairSync是公平獲取鎖。
AQS內部還有兩個內部類:Node和ConditionObject。
ConditionObject還實現了Condition接口。
5.1 AQS的API
話不多說,我們首先看看AQS的api.
請注意重點:
- AQS是一個框架
- 原子int值表示狀態
- int值只能通過getState(),setState(int)和cas更新
- AQS的等待隊列是FIFO的(CLH)
- AQS無法直接使用,需要子類實現AQS指定的方法
使用:
我們在使用AQS的框架的時候,只需要讓AQS的子類實現這些方法即可。至於等待隊列CLH的調度檢測等,AQS框架已經幫我們實現了。
這是一個典型的模板方法設計模式的使用例子。
5.2 自定義非重入獨佔鎖
我們根據API的說明,嘗試自己實現一個鎖。
當然,這個實現的鎖,是真正的鎖,不是之前使用synchronized的僞鎖。
public class MyAQS implements Lock {
private volatile Sync sync = new Sync();
private class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
// 獨佔模式獲取鎖
assert arg == 1; // arg 必須等於 1
if(compareAndSetState(0, 1)){ // 修改鎖佔用狀態成功
setExclusiveOwnerThread(Thread.currentThread()); // 設置鎖佔用線程
return true; // 鎖獲取成功
}
return false; // 鎖獲取失敗
}
@Override
protected boolean tryRelease(int arg) {
// 獨佔模式釋放鎖
assert arg == 1; // arg 必須等於 1
setExclusiveOwnerThread(null); // 清空佔用鎖線程
setState(0); // 設置鎖空閒
return true;
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
我們實現了Lock接口,在內部私有類繼承AQS框架。
我們實現的這個鎖是非重入的獨佔鎖。
獲取鎖主要做了這些操作:
檢測傳入參數
設置鎖佔用狀態
設置鎖持有線程
釋放鎖主要做了這些操作:
檢測傳入參數
清空鎖持有線程
設置鎖空閒
OK,我們創建了自己的鎖,那麼,這把鎖能不能使用呢?
我們嘗試使用
public static void main(String[] args) {
Lock lock = new MyAQS();
People people = new People();
Runnable runnable = () -> {
lock.lock();
try{
for (int i = 0;i < 1000;i++){
people.setSum(people.getSum() + 1);
}
} finally {
lock.unlock();
}
};
ExecutorService service = new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));
for(int i = 0;i < 100;i++){
service.execute(runnable);
}
service.shutdown();
try{
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException ie){
System.out.println("ie");
}
System.out.println("people sum = " + people.getSum());
}
不管執行多少次都是10W:
這個例子只是驗證了,我們的鎖在併發情況下,是排他鎖,或者說是獨佔鎖,也就是說,同一時刻,只有一個線程可以持有鎖。
之前我們還說過,MyAQS是非重入鎖:
public static void main(String[] args) {
Lock lock = new MyAQS();
Runnable runnable = () -> {
lock.lock();
try{
if(lock.tryLock()){
System.out.println("tryLock success");
lock.unlock();
} else{
System.out.println("tryLock failed");
}
} finally {
lock.unlock();
}
};
new Thread(runnable).start();
}
我們在線程內獲得鎖後,再次嘗試獲取鎖。
他核心代碼就是我們重寫AQS的tryAcquire的方法中,使用cas操作將state設置爲1.
如果是使用cas操作將state加1,那麼就是可重入鎖。
可重入鎖的釋放與獲取剛好相反,重入鎖的獲取是+1,那麼釋放就是-1.
非可重入鎖的獲取是設置爲1,那麼非可重入鎖的釋放就是設置爲0.
5.2 自定義非重入獨佔鎖的缺陷
MyAQS在獲取鎖上沒有問題,主要問題是鎖的釋放上。
釋放鎖沒有檢測當前狀態,如果已經釋放了,重複釋放鎖,是否允許。
在我們非重入鎖以及排他鎖來看,這樣是對的。
釋放鎖,沒有驗證,現在持有鎖的線程和釋放的線程是否是同一個線程。
用大白話來說:
線程A獲取了鎖,然後A線程在幹活。
線程B也來獲取鎖,然後發現鎖被人獲取了。
然後線程B調用釋放鎖的方法,此時鎖狀態就變了,然後線程B就可以獲取鎖了。
然後線程B在線程A未釋放鎖的前提下,獲取到了鎖。造成的後果就是,線程A與線程B併發執行了。
舉個例子:
public static void main(String[] args) {
Lock lock = new MyAQS();
People people = new People();
Runnable runnable = () -> {
Runnable runnable1 = () -> {
for (int i = 0; i < 1000; i++) {
people.setSum(people.getSum() + 1);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
System.out.println("ie");
}
};
if (lock.tryLock()) {
try {
runnable1.run();
} finally {
lock.unlock();
}
} else {
lock.unlock();
lock.lock();
try {
runnable1.run();
} finally {
lock.unlock();
}
}
};
ExecutorService service = new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));
for(int i = 0;i < 100;i++){
service.execute(runnable);
}
service.shutdown();
try{
TimeUnit.SECONDS.sleep(110);
}catch (InterruptedException ie){
System.out.println("ie");
}
System.out.println("people sum = " + people.getSum());
}
線程的任務是:
線程嘗試獲取鎖,如果獲取成功,那麼就執行增加people的sum的操作,增加1000次後sleep1秒,執行完成後,在finally中釋放鎖,任務結束。
如果獲取失敗,那麼調用鎖釋放操作,然後阻塞獲取鎖,獲取成功後,執行增加操作,執行完成後,在finally中釋放鎖,任務結束。
這個任務與之前的任務的不同點是,當第一次嘗試獲取鎖失敗後,會調用鎖釋放操作,然後再次獲取鎖。
接下來看下執行結果:
說實話,任務裏面加完100次後睡眠1秒,然後100個線程,總共需要睡眠100秒(至少),還是很浪費時間的。
結合之前學習的線程的知識,持有鎖,然後讓出CPU資源,可以使用yeild()替換睡眠操作。
爲什麼需要睡眠?
指令執行的速度非常快,線程睡眠是故意造成線程競爭鎖的目的。
因爲我們在鎖釋放的時候沒有驗證當前請求的線程是否是線程的持有者,造成不管有沒有持有鎖,都可以釋放鎖。就會造成例子中,惡意線程釋放其他線程的鎖,然後自己使用。
所以,我們在釋放的時候,需要驗證當前請求的線程是否持有鎖:
還記得嗎,AQS繼承AOS,而AOS很簡單,就是記錄哪個線程持有鎖(獨佔模式)
所以,我們判斷請求線程與持有線程是否一致,如果一致才釋放,否則不釋放。
我們使用修改後的鎖重試上面的例子:
使用了yeild快多了。
和我們預期相同。加入持有鎖線程判斷後,其他線程不能隨意釋放不屬於自己得到的鎖了。
5.3 自定義重入獨佔鎖
public class MyAQS1 implements Lock {
private volatile Sync sync = new Sync();
private class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
assert arg == 1;
int state = getState();
if ((Thread.currentThread() == getExclusiveOwnerThread() || getExclusiveOwnerThread() == null)
&& compareAndSetState(state, state + 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
assert arg == 1;
int state = getState();
if (Thread.currentThread() == getExclusiveOwnerThread()
&& compareAndSetState(state, state - 1)) {
setExclusiveOwnerThread(getState() == 0 ? null : getExclusiveOwnerThread());
return true;
}
return false;
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
我們定義了一個可重入的獨佔鎖。
核心方法仍然是獲取鎖和釋放鎖的方法:
獲取鎖:
所處於空閒,或者請求線程已經持有鎖
鎖狀態+1
設置鎖持有線程
釋放鎖:
請求線程持有鎖
鎖狀態-1
設置鎖持有線程,如果鎖空閒,鎖持有線程置空
這就是一個可重入的獨佔鎖
接下來用一個小例子驗證:
public static void main(String[] args) {
Lock lock = new MyAQS1();
Runnable runnable = () -> {
Thread thread = Thread.currentThread();
lock.lock();
try {
Thread.yield();
System.out.println(thread.getName() + thread.getId() + " 1 get lock");
lock.lock();
try {
Thread.yield();
System.out.println(thread.getName() + thread.getId() + " 2 get lock");
} finally {
lock.unlock();
System.out.println(thread.getName() + thread.getId() + " 2 free lock");
}
} finally {
lock.unlock();
System.out.println(thread.getName() + thread.getId() + " 1 free lock");
}
};
new Thread(runnable).start();
new Thread(runnable).start();
}
線程任務中,每次獲取到鎖,就讓出CPU時間。
看下最終的結果:
因爲獲取鎖的時候,需要驗證請求線程是否持有鎖,這裏實現的獨佔模式。
獲取鎖將鎖狀態加1,這裏實現的可重入。
可重入鎖需要注意一點,在線程中,獲取了多少次鎖,就需要釋放多少次鎖。
要保證線程退出同步的時候,鎖狀態是0
5.4 自定義門栓鎖
前面自定義的都是獨佔鎖,接下來實現一個共享鎖。
首先,門栓鎖是什麼?
想象這樣一個場景:全家人出去購物,購物回到房門口之後,發現只有媽媽帶有鑰匙,於是大家一個排一個的在們口等待。媽媽使用鑰匙打開房門後,在一個一個的進入家裏。
門栓鎖的原理就是這樣,當門栓鎖還沒有收到信號的時候,所有嘗試獲取信號的線程全部阻塞。當收到信號後,阻塞的線程競爭門栓鎖。
public class MyAQS3 {
private final Sync sync = new Sync();
private class Sync extends AbstractQueuedSynchronizer{
boolean isSignalled(){
return getState() != 0;
}
@Override
protected int tryAcquireShared(int arg) {
return isSignalled() ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
}
public boolean isSignalled(){
return sync.isSignalled();
}
public void signal(){
sync.releaseShared(1);
}
public void await() throws InterruptedException{
sync.acquireSharedInterruptibly(1);
}
}
public static void main(String[] args) {
MyAQS3 myAQS = new MyAQS3();
Runnable runnable = () -> {
myAQS.signal();
System.out.println("signal");
};
Runnable runnable1 = () -> {
Thread thread = Thread.currentThread();
try {
myAQS.await();
System.out.println("await");
} catch (InterruptedException e) {
System.out.println("ie");
}
};
System.out.println("await!!!!!");
new Thread(runnable1).start();
new Thread(runnable1).start();
try{
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ie){
System.out.println("ie");
}
System.out.println("signal!!!!!!");
new Thread(runnable).start();
}
獨佔鎖和共享鎖有一個很大區別:
獨佔鎖在同一時間只有一個線程能獲取鎖,並開始運行。
而共享鎖有一個傳播的概念,獲取到鎖的節點,會通知後面的線程獲取鎖。
我們將上面的驗證邏輯稍微修改:
public static void main(String[] args) {
MyAQS3 myAQS = new MyAQS3();
Runnable runnable = () -> {
myAQS.signal();
System.out.println("signal");
};
Runnable runnable1 = () -> {
Thread thread = Thread.currentThread();
try {
System.out.println("before await" + Thread.currentThread().getId());
myAQS.await();
System.out.println("await" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
System.out.println("ie");
}
System.out.println("after yield" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
} catch (InterruptedException e) {
System.out.println("ie");
}
};
Runnable runnable2 = () -> {
Thread thread = Thread.currentThread();
try {
System.out.println("before await" + Thread.currentThread().getId());
myAQS.await();
System.out.println("await" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ie) {
System.out.println("ie");
}
System.out.println("after yield" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
} catch (InterruptedException e) {
System.out.println("ie");
}
};
System.out.println("await!!!!!");
new Thread(runnable1).start();
new Thread(runnable2).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ie) {
System.out.println("ie");
}
System.out.println("signal!!!!!!");
new Thread(runnable).start();
}
第一個線程開始排隊,等待門栓;然後第二個線程也開始排隊,也等待門栓。
主線程睡眠2秒後,發出信號,門栓已開。
然後第一個線程開始執行,同時第二個線程也開始執行。
第一個線程獲取到門栓鎖後,睡眠1秒鐘。
第二個線程獲取到門栓鎖後,睡眠2秒鐘。
這個門栓鎖是不復位的門栓鎖,也就是說當門栓鎖收到信號後,後續的線程都可以獲取門栓鎖了。
門栓鎖不是單例。可以爲讀操作資源增加門栓鎖。
當資源準備好後,一直可以讀取。
5.5 AQS內部線程存儲數據結構–Node
通過查看源碼,發現Node主要是一個雙向鏈表。
這個Node結構,怎麼理解呢?
我們首先查看其構造方法:
發現構造主要傳入了三個信息:這個Node對應的thread和Node的模式,以及一個等待的int狀態量。
但是通過方法名和傳入的類型,對mode這個傳入的參數先保持一定的懷疑,貌似不是單純的模式。
繼續往下看:
從這裏我們可以得到兩個信息:
1.Node記錄了是獨佔的,還是共享的。
2.int型的狀態量至少有1,-1,-2,-3狀態。
狀態 | 狀態名稱 | 含義 |
---|---|---|
1 | 取消 | 已取消(因超時或者中斷,取消獲取鎖) |
0 | 新建 | 線程節點新建初始化 |
-1 | 阻塞 | 線程節點的的後續線程阻塞,當前節點釋放後需要通知後續節點 |
-2 | 等待通知 | 線程在等待通知隊列 |
-3 | 傳播 | 無條件傳播 |
繼續往下:
從這裏我們可以得到這個Node的數據結構是雙向鏈表。
在Node屬性最後,可以看到這一段代碼。前面我們知道,SHARED是使用new Node()這個構造器進行初始化的。而new Node()構造器裏面什麼都沒有,也就是說,當nextWaiter是空節點的時候,表是Node是獨佔模式。
這也是爲什麼我們看到的第二個構造器傳入的Node的參數名字是mode了。
他是由兩個屬性共同決定的。
說實話,我看到這裏是比較模糊的。到底是個啥?
不着急,稍安勿躁,我們繼續往下看。
5.6 AQS內部行爲
從5.5可以知道AQS內部數據的存儲結構,接下來我們分析下AQS內部的行爲
首先AQS是抽象類,那麼,AQS要求子類必須實現的接口是
在AQS中是沒有提供實現的,也就是說,子類如果不實現,會拋出異常。
根據我們前面他體驗的,自定義鎖的實現,可以知道:
我們最終的目的是利用AQS實現鎖,所以,接下來的方法將會圍繞Lock接口的方法進行展開。
5.6.1 tryLock
tryLock是最簡單的方法,直接調用我們Sync裏面實現的加鎖的操作。
5.6.2 lock–acquire
鎖的加鎖操作調用的是AQS的acquire方法,那麼,AQS的acquire方法幹了什麼?
這是時序圖:
首先會嘗試調用獲取鎖的操作,如果沒有獲取到鎖,那麼就會執行&&後面的操作。否則將會繼續執行。
從這裏可以看出,這不是一個公平鎖,因爲他在lock的時候會嘗試獲取鎖。如果獲取到了鎖,那麼就會執行。
5.6.2.1 tryAcquire
其中,tryAcquire是AQS的子類必須實現的操作,否則就會報UnsupportedOperationException異常。默認的tryAcquire方法內的操作是new 一個異常拋出。
5.6.2.2 AddWaiter
如果tryAcquire方法返回false,表示獲取鎖獲取失敗,然後就會執行addWaiter方法
在addWaiter中會使用當前線程創建共享的線程節點:
在addWaiter方法中,首先創建了當前線程的Node節點,然後獲取雙向隊列的尾,如果尾結點不爲空,那麼將當前節點的前繼節點設置爲尾節點,然後將新建的節點設置爲尾節點,最後將原來的尾節點的後繼節點設置爲尾節點:
①
②
③
④
5.6.2.3 enq
新增線程節點到雙向列表中,然後會執行enq方法(如果尾節點爲空,那麼表示雙向列表爲空)
如果尾節點爲空,將會新建一個空node節點,並且將空node節點放到雙向列表的頭節點,然後頭節點和尾節點相等。(第一次循環結束,注意這裏是一個死循環)
如果尾節點不爲空,就將新建節點的前繼節點設置爲尾節點,然後更新尾節點,在將原尾節點的後繼設置爲新建節點。(此時是第二次循環,同時結束循環)
爲什麼要用死循環?
因爲中間調用了CAS操作,那麼CAS操作會存在失敗的情況,如果CAS調用失敗,就會進行死循環的CAS。這裏存在自旋的操作,同時,這裏也是樂觀的,認爲CAS失敗是小概率,小時間段的操作。
如果長時間CAS失敗,這裏就會大量自旋,大量的死循環,浪費CPU資源。
5.6.2.4 acquireQueued
acquireQueued方法
final boolean acquireQueued(final Node node, int arg) { // node 是新建的節點,arg是傳入的int值
boolean failed = true; // 設置返回值是true;如果返回值是true代表着需要執行selfInterrupt();方法
try {
boolean interrupted = false; // 設置返回值是false;如果返回值是true代表着需要執行selfInterrupt();方法
for (;;) { // 這裏是死循環,自旋
final Node p = node.predecessor(); // 獲取新建節點的前繼爲當前循環節點
if (p == head && tryAcquire(arg)) { // 如果當前循環節點是頭節點那麼嘗試獲取鎖
setHead(node);// 嘗試獲取鎖成功,更新雙向列表中的頭結點爲新建節點,此時新建節點的前繼引用斷裂
p.next = null; // help GC // 原頭結點獲取到鎖,將其從雙向列表中移除,
//因爲列表中前繼引用已斷裂,所以將移除節點的後繼引用斷裂,此時移除節點已經不再雙向列表中了
// 本次循環結束,移除節點已經是一個可gc的對象了
failed = false; // 因爲新建節點還在雙向列表中,雙向列表不爲空,且本次循環已經有節點獲取到鎖了,所以不需要取消
return interrupted;// 返回false,表示不需要執行selfInterrupt方法
}
if (shouldParkAfterFailedAcquire(p, node) && // 清理雙向列表中取消的線程節點
parkAndCheckInterrupt()) // 檢測是否中斷
interrupted = true; // 返回true;表示需要執行selfInterrupt
}
} finally {
if (failed) // 如果獲取鎖失敗,那麼將雙向列表中所有的節點都設置爲取消
cancelAcquire(node);
}
}
5.6.2.5 predecessor
獲取前繼節點,或者拋出NPE.
5.6.2.6 shouldParkAfterFailedAcquire
在每次自旋中,如果沒有獲取鎖,那麼更新雙向列表中可以釋放的節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 獲取前繼節點的等待狀態
if (ws == Node.SIGNAL) // 如果前繼節點的等待狀態是 SIGNAL 也就是-1
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true; // 返回true 將會執行parkAndCheckInterrupt方法
if (ws > 0) { // ws > 0 那麼就表示ws = 1 ,即pred節點是取消節點:CANCELLED
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; // node 越過前繼節點,同時pred前移
// 如果node的前繼節點的等待狀態是取消,那麼忽略這個節點,知道前繼節點不是取消的
// 這裏需要注意,頭結點是一個空node,空node的等待狀態是沒有設置的,但是因爲
// waitStatus是int類型的,默認是0
} while (pred.waitStatus > 0); // 保證node的前繼節點一定不是取消節點
pred.next = node; // 前繼節點的後繼節點是node(忽略了取消節點)
} else { // ws <= 0 ,那麼可以是 0,-2,-3
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 將前繼節點的狀態設置爲 -1 SIGNAL,等待通知
}
return false;
}
5.6.2.7 parkAndCheckInterrupt
阻塞線程,並返回線程中斷標誌
調用LockSupport.park方法,阻塞線程,然後清除線程的中斷標誌。
然後返回線程的中斷狀態,並重置線程的中斷標誌。
5.6.2.8 cancelAcquire
如果雙向列表的頭結點在獲取鎖的死循環中失敗,就會退出死循環:
在predecessor中會拋出異常,只有當新增節點前全部是取消節點或者雙向列表爲空的時候,新增節點才能訪問頭結點。
也只有新增節點的前繼節點是頭結點(忽略取消節點)
就該執行finally中的代碼了。
如果頭節點獲取鎖失敗,就會執行cancelAcquire方法。
cancelAcquire方法是將嘗試獲取鎖的節點設置爲取消狀態
(只有頭結點獲取鎖失敗,纔會觸發,在acquire中,node是新增的節點)
(如果新增節點的前繼不是頭結點,那麼就會就會將前繼節點的等待狀態設置爲SIGNAL)
然後阻塞線程。阻塞線程後設置中斷標誌爲true.最後會返回中斷狀態。
返回的中斷狀態會影響是否執行selfInterrupt方法。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
// 如果node爲空,表示node所屬的線程現在已經處於阻塞狀態
// 所以,忽略這個阻塞的線程節點,並且終止取消獲取鎖方法
if (node == null)
return;
// 能進入這個方法,一般就是新增的線程節點中的線程被中斷,拋出中斷異常,
// 此時就需要將已經加入到雙向列表的線程節點取消
node.thread = null; // 取消線程節點與線程的關聯
// Skip cancelled predecessors
Node pred = node.prev; // 得到新增線程節點的前繼(忽略取消節點)
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next; // 得到新增線程節點的後繼節點
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; // 新增線程節點設置爲取消
// If we are the tail, remove ourselves.
// 如果新增線程節點是尾結點,那麼就將新增線程節點的前繼設置爲尾節點
if (node == tail && compareAndSetTail(node, pred)) {
// 如果尾節點更新成功,那麼就將尾節點的後繼設置爲空
compareAndSetNext(pred, predNext, null);
} else {
// 如果新增線程節點不是尾節點
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head && // 新增線程節點的前繼不是頭節點(雙向列表中節點數量大於1)
((ws = pred.waitStatus) == Node.SIGNAL || // (新增線程節點的前繼節點的等待狀態是SIGNAL)
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && // ws < 0,那麼在這裏,ws一定等於 -1 也就是SIGNAL,
//但是因爲併發,每次比較之後,值都有可能改變,所以使用ws < 0多判斷一次
pred.thread != null) { // 線程節點所屬線程不爲空
Node next = node.next; // 得到新增線程節點的後繼線程節點
if (next != null && next.waitStatus <= 0) // 如果新增線程節點的後繼線程節點不爲空
// 且後繼線程節點也不是取消的線程節點,那麼就將前繼節點的後繼節點設置爲新增線程的後繼節點
compareAndSetNext(pred, predNext, next);
} else {
// 到了這裏,表示新增線程節點是雙向列表的頭,所以,喚醒新增線程節點中的線程
unparkSuccessor(node);
}
// 新增線程節點的後繼設置爲自己
// 我認爲隱藏的含義是告訴GC,沒有人引用我了,如果內存不足,請回收我吧。
node.next = node; // help GC
}
}
5.6.2.9 unparkSuccessor
喚醒線程節點中的線程
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus; // 獲取線程節點的等待狀態
if (ws < 0) // 如果線程節點的等待狀態小於 0 ,也就是 -1,-2,-3
compareAndSetWaitStatus(node, ws, 0); // 那麼就將線程節點的等待狀態設置爲0
// 0 不僅僅是新建狀態,也是線程節點得到鎖的標誌
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next; // 得到後繼線程節點
if (s == null || s.waitStatus > 0) { // 如果後繼線程節點爲空或者後繼線程節點是取消節點
s = null; // 忽略取消狀態的後繼線程節點
for (Node t = tail; t != null && t != node; t = t.prev) // 遍歷雙向鏈表,從後往前遍歷
if (t.waitStatus <= 0) // 找到最前面的非取消狀態的線程節點
s = t;
}
if (s != null) // 如果雙向鏈表中最前面的非取消線程節點不爲空
LockSupport.unpark(s.thread); // 那麼喚醒這個線程節點所屬的線程
}
5.6.2.10 selfInterrupt
selfInterrupt方法就是很簡單的將線程進行中斷。
爲什麼要中斷呢?
得到了中斷標誌,就要在外層進行中斷線程:
5.6.3 unlock-release
首先,unlock中調用的是AQS中的release方法
這是AQS中release方法的時序圖
比起lock方法,這個可就簡單多了。
release是鎖釋放的邏輯,代碼很少,我們來看下鎖是如何釋放的
tryRelease是AQS子類必須實現的方法,如果沒有實現,將會拋出UnsupportedOperationException異常。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 嘗試釋放鎖
Node h = head; // 如果釋放成功,那麼得到雙向列表的頭節點
if (h != null && h.waitStatus != 0) // 如果頭結點不爲空,且頭結點還未獲取到鎖
unparkSuccessor(h); // 那麼就喚醒頭結點(內部會將雙向鏈表從尾到頭便利一次,找到最前面的非取消的節點)
return true;
}
return false;
}
5.6.4 lockInterruptibly-acquireInterruptibly
這個和lock的區別在於,lock是自旋阻塞獲取鎖,如果獲取不到鎖,就阻塞了。在5.6.2小節中,我們看了lock的具體過程。
其中有這樣一段操作:
發現線程被中斷了,會在外層進行中斷線程。也就是說,自旋是不可被打斷的,即使我們發出了線程中斷信號,線程也只是會在獲取到鎖之後進行中斷。
也就是說,在自旋獲取鎖的期間,線程是不響應中斷信號的。
而acquireInterruptibly方法和lock裏面的acquireQueued方法類似,只不過當得到中斷信號後,會將異常拋出,將中斷異常的捕獲和之後的操作交給調用者實現。
acquireInterruptibly的方法:
如果線程的中斷標誌已經是中斷了,那麼就直接拋出異常,快速結束。
否則先嚐試獲取一次鎖,如果獲取失敗,那麼就進入doAcquireInterrupttibly方法。
doAcquireInterruptibly方法和acquireQueued方法只有紅框中的操作不一樣,其他的都一模一樣。
正式利用了throw拋出異常,使得線程在自旋獲取鎖的過程中能夠響應中斷信號。
5.6.5 tryLock(long time, TimeUtil unit)-tryAcquireNanos
嘗試獲取鎖,還有一個有超時時間的方法,在子類裏面調用的實際上是AQS的tryAcquireNanos方法
首先判斷線程的中斷標誌,如果中斷標誌已中斷,那麼,直接拋出中斷異常,快速結束。
否則先調用tryAcquire嘗試獲取鎖,如果沒有獲取到鎖,那麼執行doAcquireNanos。
這個方法除了紅框標記的,其他的和acquireQueued方法相同。所以,我們只看紅框中不一樣的邏輯。
首先傳入了一個long類型的納秒值。
如果等待的納秒值小於等於0,直接結束。
否則就會獲取當前系統時間的納秒值,然後計算結束的納秒值:
結束時間=當前時間+等待時間
在自旋里面,每一次自旋都使用結束時間減去當前系統時間,然後判斷結果。
如果差值小於等於0,那麼表示時間到了,應該結束。
如果差值大於閾值
閾值1毫秒
那麼阻塞線程。
還會判斷線程的中斷標誌,如果中斷標誌已中斷,那麼拋出異常,快速結束。
5.6.6 await-acquireShared
await是共享鎖等待信號的方法。
其內部調用了AQS的acquireShared方法:
也是先進去的時候會嘗試獲取鎖,如果獲取到鎖變量小於0,那麼就會調用doAcquireShared方法。
也就是說,tryAcquireShared如果返回的值 >= 0 那麼就獲取到了共享鎖。
這是acquireShared的時序圖:
如果沒有獲取到鎖,那麼就會將新建的節點加入到雙向列表中調用addWaiter方法,將新增的線程節點加入到雙向列表尾部。
然後進入自選循環。
接着調用predecessor方法,獲取新增的線程節點的非取消前繼節點。
如果前繼節點是頭節點(意味着雙向列表中前面的線程節點已經獲取到了鎖,現在新增的線程節點的前繼節點是雙向列表的頭),而新增線程節點的前繼也得到了鎖,那麼就會將前繼線程節點移出雙向列表,同時將新增的線程節點設置爲頭結點。
如果設置了新增線程節點爲頭結點,而且有如下情況的:
a. 共享鎖資源大於0(表示頭結點也能夠獲取鎖)
b. 頭結點爲空(其他線程已經讓頭結點獲取到了鎖且已經將頭節點移出雙向列表)
c. 頭結點不是取消的線程節點
那麼就會得到新增線程節點的後繼線程節點。
如果後繼節點也是共享鎖的線程節點,那麼調用doReleaseShared方法:
這個方法是先獲取到頭結點,如果頭結點不等於空,且雙向列表的節點數量不等於1(如果頭結點和尾節點重合,那麼表示雙向列表的節點數量爲1,因爲雙向列表的頭節點初始化是一個空節點)那麼就獲取頭結點的等待狀態,如果頭結點的等待狀態是SIGNAL,那麼就將頭結點的等待狀態設置爲0,即得到了鎖,然後喚醒頭結點所屬的線程。
如果頭結點的等待狀態是0,那麼設置頭結點的等待狀態是傳播。
這裏我是這樣理解的:
現在新增的線程節點在雙向列表中是第二個節點,此時第一個節點獲取到了鎖,然後會將頭結點移出雙向列表,同時將新增的線程節點設置爲頭結點。
如果新增線程節點有後繼節點,對於後繼節點來說,如果頭結點的等待狀態SIGNAL,那麼就將頭結點設置爲已經得到了鎖(實際上是頭結點之後的節點,頭結點是空節點),同時喚醒頭結點所屬線程;如果頭結點已經獲取到了鎖,那麼將頭節點的等待狀態設置爲傳播狀態。這裏應該是自旋進行處理能夠獲取到鎖的線程節點。
剩下的這些方法和acquireQueued方法中的一樣處理了。
5.6.7 releaseShared
這個共享鎖釋放的方法就很簡單,如果tryReleaseShared方法返回false就是釋放失敗,否則就是釋放成功。釋放成功會自旋等待雙向列表中共享線程節點全部運行。
6. 總結
寫了這麼多,基本上將AQS的代碼閱讀了一遍,我們就總結下:
- AQS內部存儲是一個雙端的雙向列表
- 雙向列表的等待狀態是1,0,-1,-2,-3
- 雙向列表總是忽略取消狀態的節點
- 不管是獲取鎖還是釋放鎖都是自旋操作
- 線程節點新增入雙向列表後,會使線程調度暫時禁用,即暫時阻塞線程
- 線程節點獲取鎖後,將線程調度設置爲可用,即喚醒阻塞線程
- 獨佔鎖實現tryAcquire和tryRelease方法即可
- 共享鎖實現tryAcquireShared和tryReleaseShared方法
- 獲取鎖的方法tryAcquire方法返回true表示獲取到了鎖
- 獲取鎖的方法tryAcquireShared返回的值大於等於0表示獲取到了鎖
- 釋放鎖的方法tryRelease方法返回true表示釋放了鎖
- 釋放鎖的方法tryReleaseShared方法返回true表示釋放了鎖
AQS的調用鏈很長,設計非常巧妙,看明白AQS的源碼非常困難。經常是看着看着就看不懂爲什麼了,需要閱讀很多遍才能明白設計的巧妙。我從頭開始閱讀源碼,到看到這裏花費了大概2個周多一點(下班時間閱讀,平均每天2小時,期間還有一個周重裝了操作系統)。但是讀懂AQS的源碼很值得,後面再閱讀jdk中的鎖的時候,就不會很喫力了。