面試必考AQS-AQS源碼全局分析

源碼劃分

在《面試必考AQS-AQS概覽》中我將AQS的源碼,大致分爲六部分,總結一下分別是:

  • 鏈表節點類、鏈表頭尾指針
  • 條件對象
  • 同步器狀態/資源
  • 用於cas操作的變量及方法
  • 一些實例方法操作鏈表及同步器狀態狀態
  • 一些抽象方法等待被實現

接下來就逐個介紹這些內容。

抽象方法介紹

AQS中包含5個未實現方法,也就是等待子類來實現個性化邏輯的,他們是:

protected boolean tryAcquire(int arg); // 嘗試獲取排他鎖
protected boolean tryRelease(int arg); // 嘗試釋放排他鎖
protected int tryAcquireShared(int arg); // 嘗試獲取共享鎖
protected boolean tryReleaseShared(int arg); // 嘗試釋放共享鎖
protected boolean isHeldExclusively(); // 判斷當前線程是否持有排他鎖

由這5個方法可知,AQS中涉及了排它鎖和共享鎖兩類的操作,每種鎖又包含申請和釋放兩類操作流程,每個流程中又涉及到多個實例方法和抽象方法調用。

在AQS中,方法分爲可中斷的、不可中斷的、同步的、帶超時時間的同步的,目前整理的流程是“不可中斷的同步的”。

實例方法介紹

排他鎖的獲取流程

排它鎖,又稱獨佔鎖,同一時刻只能有一個線程申請到,其他線程進入同步隊列,等待鎖釋放後,按隊列順序申請排它鎖;涉及到的方法及功能依次爲:

public final void acquire(int arg){...} // 獲取排它鎖的入口
# protected boolean tryAcquire(int arg); // 嘗試直接獲取鎖
final boolean acquireQueued(final Node node, int arg) {...} // AQS中獲取排它鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected boolean tryAcquire(int arg); // 如果當前node的前置節點pre變爲了head節點,則嘗試獲取鎖(因爲head可能正在釋放)
private void setHead(Node node) {...} // 設置 同步隊列的head節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除
static void selfInterrupt() {...} // 操作將當前線程中斷

看上面的流程中,涉及到2次抽象方法tryAcquire(int arg)的調用,也就是在申請過程中需要實現類的參與,它決定了達到何種狀態纔算申請成功。

排他鎖的釋放流程

釋放的調用流程較爲簡單,涉及到的方法及功能依次爲:

public final boolean release(int arg) {...} // 釋放排它鎖的入口
# protected boolean tryRelease(int arg) ; // 嘗試直接釋放鎖
private void unparkSuccessor(Node node) {...} // 喚醒後繼節點

共享鎖的獲取流程

共享鎖,允許同一時刻有多個線程持有,但共享鎖的申請會考慮當前排它鎖的狀態,也就是當存在排它鎖時,共享鎖的申請也要進入同步隊列等待,直到排它鎖被釋放,才真正去申請;涉及到的方法及功能依次爲:

public final void acquireShared(int arg) {...} // 獲取共享鎖的入口
# protected int tryAcquireShared(int arg); // 嘗試獲取共享鎖
private void doAcquireShared(int arg) {...} // AQS中獲取共享鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected int tryAcquireShared(int arg); // 嘗試獲取共享鎖
private void setHeadAndPropagate(Node node, int propagate) {...} // 設置 同步隊列的head節點,以及觸發"傳播"操作
# private void doReleaseShared() {...} // 遍歷同步隊列,調整節點狀態,喚醒待申請節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起 
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除

共享鎖的釋放流程

釋放的調用流程較爲簡單,涉及到的方法及功能依次爲:

public final boolean releaseShared(int arg) {...} // 釋放共享鎖的入口
# protected boolean tryReleaseShared(int arg); // 嘗試釋放共享鎖
private void doReleaseShared() {...} // 遍歷同步隊列,調整節點狀態,喚醒待申請節點

在細緻讀完AQS中申請和釋放鎖的簡略流程後,會發現裏面涉及到大量針對Node和同步隊列的處理。這就涉及到CLH模型了,該模型的介紹可閱讀《面試必考AQS-AQS概覽》。

下面我們熟悉下Node類的構成。

Node類

在AQS中一直反覆提到同步隊列,這個是CLH模型中鏈表的實現。而同步隊列中加入的節點元素,就是Node實例,裏面包含了申請鎖的線程信息、節點狀態、前後節點的指針等。關於Node類,可以直接看一下定義內容:

構造函數

// 初始化head節點或shared節點使用
Node() {    
}
// CLH模型的同步隊裏中使用
Node(Thread thread, Node mode) {     
	this.nextWaiter = mode;
	this.thread = thread;
}
// Condition的等待隊列中使用
Node(Thread thread, int waitStatus) { 
	this.waitStatus = waitStatus;
	this.thread = thread;
}

三個構造函數,根據註釋可知,在鎖操作期間涉及到的有2個,分別是無參構造函數Node()和包含線程及節點的Node(Thread thread, Node mode)。在實際使用過程中,Node mode入參使用的是Node SHARED和Node EXCLUSIVE兩個對象

常量字段

static final Node SHARED = new Node(); // 標識共享鎖節點
static final Node EXCLUSIVE = null; // 標識排它鎖節點
// 以下常量,都是waitStatus字段可能的值
static final int CANCELLED =  1; // 取消狀態,也就是不想申請或釋放了
static final int SIGNAL    = -1; // 等待通知狀態,當節點變爲這個狀態,說明後繼節點等待被喚醒
static final int CONDITION = -2; // 條件狀態,await/signal 場景下使用
static final int PROPAGATE = -3; // 傳播狀態,這個很diao,需要長篇幅的介紹思路

四個int類型的常量,一定要牢記,後面的流程中會經常涉及到waitStatus值的判斷,在這裏可以先劃分一下類型,值小於0的都可以認爲值等待狀態(不同的值涉及到不同的情況及用法),而大於0的則是無效狀態,需要將節點從同步隊列中處理掉。

變量字段

volatile int waitStatus; // 用於標識節點狀態
volatile Node prev; // 前置節點指針
volatile Node next; // 後繼節點指針
volatile Thread thread; // 持有的線程對象,可以理解爲每個node代表一個線程
Node nextWaiter; // 等待隊列下一個節點指針(Condition中使用)

這裏的waitStatus則是當前節點的具體狀態;prev和next則是兩個指針,用於快速操作前後節點;thread則是node對象持有的線程對象,也就是當前是哪個線程準備申請/釋放鎖;至於nextWaiter是在Condition中使用的,這個將來介紹await和signal的時候再做介紹。

方法

// 判斷是否爲申請共享鎖的節點;
final boolean isShared() {
	return nextWaiter == SHARED;
}

// 獲取前置節點,如果爲null拋出異常
final Node predecessor() throws NullPointerException {
	Node p = prev;
	if (p == null)
		throw new NullPointerException();
	else
		return p;
}

關於Node類的實例方法,只有2個,一個是判斷當前節點是否爲共享類型的節點,另一個是獲取當前節點的前置節點對象。

以上就是Node類的所有成員,在申請和釋放鎖的流程流程中涉及到大量對它們的操作。

AQS成員變量

在AQS當中,申請鎖過程中除了CAS相關成員外,核心的成員變量就三個:

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state; // 同步器狀態,它的值決定了是否持有鎖

其中head和tail就是同步隊列的頭尾,對於head,就是當前申請到鎖的節點,而tail則是指向新加入同步隊列的節點。在每次申請到鎖時會調整head的指向,釋放鎖時會將head指向下一個節點。而新節點入隊則是採用尾插方式,並且調整tail節點指向新插入的node對象。

總結

本文主要是對AQS的源碼做了一次比較籠統的概念,是基於對AQS有一定熟悉程度的前提下描述的,建議讀者在未來閱讀完本系列後,回到這篇文章,對AQS的整體再加深一次理解。後續將會對AQS中鎖的申請與釋放詳細分析其源碼邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章