AQS源碼導讀

前言

AQS全稱:AbstractQueuedSynchronizer,抽象的隊列同步器,和synchronized不一樣的是,它是使用Java編寫實現的一個同步器,開發者能夠基於它進行功能的加強和擴展。java

AQS堪稱J.U.C包的半壁江山,不少併發工具類都是使用AQS來實現的,例如:ReentrantLock、Semaphore、CountDownLatch等。node

使用synchronized實現同步的原理是:每一個Java鎖對象都有一個對應的Monitor對象(對象監視器),Monitor對象是由C++實現的。對象監視器維護了一個變量Owner,指向的是當前持有鎖的線程,還維護了一個Entry List集合,存放的是競爭鎖失敗的線程。線程在這個集合裏會被掛起休眠,直到Owner線程釋放鎖,JVM纔去Entry List集合中喚醒線程來繼續競爭鎖,循環往復。面試

AQS的任務就是使用Java代碼的方式,去完成synchronized中由C代碼實現的功能。Java開發者不必定熟悉C語言,要讀懂synchronized的源碼實現並不是易事。不過好在JDK提供了AQS,經過閱讀AQS的源碼也能讓你對併發有更深的理解。算法

AQS的核心思想是:經過一個volatile修飾的int屬性state表明同步狀態,例如0是無鎖狀態,1是上鎖狀態。多線程競爭資源時,經過CAS的方式來修改state,例如從0修改成1,修改爲功的線程即爲資源競爭成功的線程,將其設爲exclusiveOwnerThread,也稱【工做線程】,資源競爭失敗的線程會被放入一個FIFO的隊列中並掛起休眠,當exclusiveOwnerThread線程釋放資源後,會從隊列中喚醒線程繼續工做,循環往復。 邏輯是否是和synchronized底層差很少?對吧。markdown

理論說的差很少了,本篇文章就經過ReentrantLock結合AbstractQueuedSynchronizer,經過閱讀源碼的方式來看一下AQS究竟是如何工做的,順便膜拜一下Doug Lea大佬。多線程


AQS基礎架構

閱讀源碼前,先來簡單瞭解一下AQS的架構: 在這裏插入圖片描述 架構仍是比較簡單的,除了實現Serializable接口外,就只繼承了AbstractOwnableSynchronizer父類。 AbstractOwnableSynchronizer父類中維護了一個exclusiveOwnerThread屬性,是用來記錄當前同步器資源的獨佔線程的,沒有其餘東西。架構

AQS有一個內部類Node,AQS會將競爭鎖失敗的線程封裝成一個Node節點,Node類有prevnext屬性,分別指向前驅節點和後繼節點,造成一個雙向鏈表的結構。除此以外,每一個Node節點還有一個被volatile修飾的int變量waitStatus,它表明的是節點的等待狀態,有以下幾種值:併發

  1. 0:新建節點的默認值。
  2. SIGNAL(-1):表示後繼結點在等待當前結點喚醒。
  3. CONDITION(-2):表示結點等待在Condition上,當其餘線程調用了Condition的signal()方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  4. PROPAGATE(-3):共享模式下,前驅節點會喚醒全部的後繼節點。
  5. CANCELLED(1):取消競爭資源,鎖超時或發生中斷纔會觸發。

能夠看到,waitStatus是以0爲臨界值的,大於0表明節點無效,例如AQS在喚醒隊列中的節點時,waitStatus大於0的節點會被跳過。app

AQS內部還維護了int類型的state變量,表明同步器的狀態。例如,在ReentrantLock中,state就表明鎖的重入次數,每lock一次,state就+1,每unlock一次,state就-1,當state等於0時,表明沒有上鎖。ide

AQS內部還維護了headtail屬性,用來指向FIFO隊列中的頭尾節點,被head指向的節點,老是工做線程。線程在獲取到鎖後,是不會出隊的。只有當head釋放鎖,並將其後繼節點喚醒並設爲head後,纔會出隊。


ReentrantLock.lock()作了什麼?

示例程序:開啓三個線程:A、B、C,按順序依次調用lock()方法,這期間到底發生了什麼???

一、剛開始沒有任何線程競爭鎖,AQS內部結構是這樣的: 在這裏插入圖片描述 二、線程A調用lock()方法: 在這裏插入圖片描述 實際上是交給sync對象去上鎖了,Sync類就是一個繼承了AQS的類。

ReentrantLock默認採用的是非公平鎖,無論隊列中是否有等待線程,上來直接就嘗試利用CAS搶鎖,若是搶成功了,就將當前線程設爲exclusiveOwnerThread並返回。 若是沒有成功,則調用acquire(1)去獲取鎖。

// 非公平鎖的lock,上來直接就搶鎖,無論隊列中有沒有線程在等待。
final void lock() {
	// CAS的方式將修改state,若是修改爲功,表示沒有其餘線程持有鎖,將當前線程設爲獨佔鎖的持有者
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		// CAS失敗,表明其餘線程已經持有鎖了,此時去競爭鎖
		acquire(1);
}
複製代碼

公平鎖就顯得很是有禮貌,上來先詢問隊列中是否有線程在等待,若是有,則讓它們先獲取,本身入隊等待。

final void lock() {
	acquire(1);
}

// 公平鎖-獲取鎖
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		/* 即便當前是無鎖狀態,也要判斷隊列中是否有線程已經在等待了。 若是有其餘線程在等待,要讓其餘線程先獲取鎖,本身入隊掛起。 若是隊列中無線程,則嘗試CAS競爭。 */
		if (!hasQueuedPredecessors() &&
				compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 當前線程就是持有鎖的線程,重入便可
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0)// 重入次數過多,溢出了
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}
複製代碼

無論是公平鎖仍是非公平鎖,線程A此時就能夠獲取到鎖並返回了,此時AQS的內部結構以下: 在這裏插入圖片描述

假設線程A還未釋放鎖,線程B調用lock(),競爭鎖失敗,則調用acquire(1)去獲取鎖,這是AQS的模板方法。

/* 競爭鎖的流程: 1.tryAcquire():再次嘗試去獲取鎖。 2.addWaiter():若是還獲取不到,在隊列的尾巴添加一個Node。 3.acquireQueued():去排隊。 */
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		// 是否須要進行一次自我中斷,來補上線程等待期間發生的中斷。
		selfInterrupt();
}
複製代碼

在acquire()方法中,首先會調用tryAcquire()去嘗試獲取鎖,若是獲取不到,則經過addWaiter()將當前線程封裝爲一個Node節點入隊,再調用acquireQueued()去排隊。 這裏有一點須要注意,AQS在排隊的過程當中,是不響應中斷的,若是排隊期間發生了中斷,只能等排隊結束後,AQS自動補上一個自我中斷:selfInterrupt()。

非公平鎖嘗試獲取鎖的流程以下:

// 非公平鎖嘗試獲取鎖
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// state==0,表明其餘線程已經釋放鎖了,再次CAS的方式修改state,成功則表明搶到鎖,返回。
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 其餘線程還沒釋放鎖,判斷持有鎖的線程是不是當前線程,若是是,則重入,state++。
	// 可重入鎖,state就表明鎖重入的次數,0說明鎖釋放了。
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) // 鎖不可能無限重入,重入的次數超過了int最大值後,就會拋異常。
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	// 其餘線程沒釋放鎖,當前線程又不是持有鎖的線程,則搶鎖失敗。
	return false;
}
複製代碼

因爲線程A沒有釋放鎖,且線程B不是鎖的持有線程,所以tryAcquire()會返回false。

嘗試獲取鎖失敗,則開始建立Node節點,併入隊。addWaiter代碼以下:

// 若是嘗試獲取鎖失敗,則入隊。
private Node addWaiter(Node mode) {
	// 建立一個和當前線程綁定的Node節點
	Node node = new Node(Thread.currentThread(), mode);
	Node pred = tail;
	if (pred != null) {
		/* 若是tail不爲null,則經過CAS的方式將tail指向當前Node。若是失敗,會調用enq()重試。 1.當前節點的prev指向前任tail 2.CAS將tail指向當前節點 3.前任tail的next指向當前節點 這三個步驟不是原子的,若是執行到第2步時間片到期,持有鎖的線程釋放鎖喚醒節點時, 若是從head向tail找,此時前任tail節點的next仍是null,會存在漏喚醒問題。 而prev的賦值先於CAS執行,因此在喚醒隊列時,從tail向head找就沒問題了。 */
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	// 若是CAS入隊失敗,則自旋重試
	enq(node);
	return node;
}
複製代碼

若是tail不爲null,則將當前節點的prev指向現任tail,再經過CAS的方式將tail指向當前節點,最後前任tail的next指向當前節點便可。 這裏有一點須要注意:

  1. 當前節點的prev指向前任tail
  2. CAS將tail指向當前節點
  3. 前任tail的next指向當前節點

這三個步驟不是原子的,若是執行到第2步時間片到期,持有鎖的線程釋放鎖喚醒節點時,若是從head向tail找,此時前任tail節點的next仍是null,會存在漏喚醒問題。而prev的賦值先於CAS執行,因此在喚醒隊列時,從tail向head找就沒問題了。

若是CAS入隊失敗也不要緊,下面會調用enq()進行自旋重試,直到成功爲止:

// CAS入隊失敗,自旋重試
private Node enq(final Node node) {
	for (;;) { // 這比while(true)好在哪裏???
		Node t = tail;
		if (t == null) {
			// tail==null,說明隊列是空的,作初始化。
			// 新建一個節點,head和tail都指向它。再進循環時,tail就不爲null了。
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			/* 隊列不爲空 1.當前節點的prev指向前任tail 2.CAS將tail指向當前節點 3.前任tail的next指向當前節點 不斷重試,直到成功爲止 */
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}
複製代碼

若是tail爲null,說明隊列還沒初始化,這時會建立一個Node節點,head和tail都指向這個空節點。再次循環時,因爲已經初始化了,進入else邏輯,仍是執行那三個步驟。

線程B入隊後,此時AQS的內部結構以下: 在這裏插入圖片描述

節點B成功入隊後,就是排隊的操做了。線程B是繼續競爭仍是Park掛起呢?

// Node入隊後,開始排隊
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;// 是否獲取鎖失敗
	try {
		/* 線程等待的過程當中是不響應中斷的,若是期間發生中斷, 則必須等到線程搶到鎖後進行自我中斷:selfInterrupt()。 */
		boolean interrupted = false;//獲取鎖的過程當中是否發生中斷。
		for (;;) {
			final Node p = node.predecessor();// 獲取當前節點的前驅節點
			/* 若是本身的前驅是head,本身就有資格去搶鎖,有兩種狀況: 一、做爲第一個節點入隊。 二、head釋放鎖了,喚醒了當前節點。 */
			if (p == head && tryAcquire(arg)) {
				/* 若是搶鎖成功,說明是head釋放鎖並喚醒了當前節點。 將head指向當前節點,failed = false表示成功獲取到鎖。 */
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			// 競爭鎖失敗,判斷是否須要掛起當前線程
			if (shouldParkAfterFailedAcquire(p, node) &&
					parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		// 獲取不到鎖就掛起線程,不斷死循環,所以只要不出意外,最終必定能獲取到鎖。
		// 若是沒有獲取到鎖就跳出循環了,說明線程不想競爭了,例如:鎖超時。
		// 此時須要修改
		if (failed)
			cancelAcquire(node);
	}
}
複製代碼

若是當前節點的前驅節點是head,則表明當前節點擁有競爭鎖的資格。分兩種狀況:

  1. 做爲第一個節點入隊。
  2. head釋放鎖了,喚醒了當前節點。

此時線程B並非被線程A喚醒了,而是第一種狀況,線程B會再次嘗試獲取鎖,可是因爲線程A還沒釋放,所以會失敗。 線程B獲取鎖失敗後,會執行shouldParkAfterFailedAcquire(),判斷是否應該被Park掛起。

// 線程競爭鎖失敗是否要掛起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 若是前驅節點的waitStatus=-1,當前節點就能夠安心掛起了。
		return true;
	if (ws > 0) {
		// waitStatus以0爲分界點。0是默認值,小於0表明節點爲有效狀態,大於0表明節點無效,例如:被取消了。
		// 若是前驅節點無效,就繼續向前找,直到找到有效節點,並將其next指向本身。中間無效的節點會被GC回收。
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {
		// CAS的方式將前驅節點的waitStatus改成-1,表明當前節點在等待被前驅節點喚醒。
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}
複製代碼

線程被Park掛起的前提條件是:必須將前驅節點的waitStatus設爲SIGNAL(-1),這樣當前驅節點釋放鎖時,纔會喚醒後繼節點。

因爲此時head節點的waitStatus等於0,不知足條件,因此線程B會嘗試使用CAS的方式將其改成SIGNAL,且這一次不會線程B不會被Park。 此時,AQS的內部結構是: 在這裏插入圖片描述

再次循環,因爲線程A還沒釋放鎖,線程B在此獲取鎖失敗,再次執行shouldParkAfterFailedAcquire(),此時前驅節點的waitStatus已是SIGNAL(-1)了,因此線程B能夠安心Park了,返回true。

shouldParkAfterFailedAcquire()返回true表明須要將線程B掛起,所以會執行parkAndCheckInterrupt()

// 掛起當前線程,等待被前驅節點喚醒
private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
	// 阻塞過程當中不響應中斷,期間若是發生了中斷,則補上自我中斷:selfInterrupt()。
	return Thread.interrupted();
}
複製代碼

掛起的過程就很簡單了,調用了LockSupport.park()方法。前面已經說過,AQS排隊的過程是不響應中斷的,若是期間發生了中斷,只能等待線程被喚醒後,補上自我中斷,因此這裏會返回線程的一箇中斷標誌。

線程B如今已經被Park掛起了,只能等待線程A的喚醒才能繼續運行。

acquireQueued()方法中,有一個finally語句塊,它的做用是,若是線程沒有獲取到鎖就退出了循環,說明線程獲取鎖超時或者發生中斷了,那麼節點就無效了,須要將它出隊,調用cancelAcquire()

// 節點取消競爭鎖
private void cancelAcquire(Node node) {
	// 忽略不存在的節點
	if (node == null)
		return;

	node.thread = null;//取消綁定的線程

	// 往前找,跳過CANCELLED的節點
	Node pred = node.prev;
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;

	Node predNext = pred.next;

	// 將當前節點設爲CANCELLED,這樣即便出隊失敗也不要緊,喚醒節點時會跳過CANCELLED的節點
	node.waitStatus = Node.CANCELLED;

	if (node == tail && compareAndSetTail(node, pred)) {
		/* 若是node是尾巴,就使用CAS將tail指向前驅節點,當前節點直接出隊。 出隊成功,將tail的next置空。 */
		compareAndSetNext(pred, predNext, null);
	} else {
		/* 若是前面的操做失敗了,有兩種狀況: 1.當前node原來是尾巴,取消過程當中有新節點插入,現已不是尾巴。 2.當前node原來就是中間節點。 當前node是中間節點的話,就須要作兩件事: 1.修改前驅節點的waitStatus爲SIGNAL,讓其釋放鎖後記得喚醒後繼節點。 2.將前驅節點的next指向後繼節點,當前node出列。 出列的過程是容許失敗的,即便沒有出列,只要node的waitStatus設爲CANCELLED, head在喚醒後繼節點時也會跳過CANCELLED的節點。 修改前驅節點爲SIGNAL的過程也是容許失敗的,只要失敗了就會喚醒node的後繼節點, 讓後繼節點本身去修改前驅節點爲SIGNAL。 */
		int ws;
		if (pred != head &&
				((ws = pred.waitStatus) == Node.SIGNAL ||
						(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
				pred.thread != null) {
			/* 修改前驅節點爲SIGNAL成功,將前驅節點的next指向當前節點的後繼節點,當前節點出列。 */
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				compareAndSetNext(pred, predNext, next);
		} else {
			/* 失敗有兩種狀況: 1.當前節點的prev爲head,那老二就有資格去競爭了,喚醒當前節點的後繼節點。 2.修改前驅節點爲SIGNAL失敗,喚醒後繼節點,讓它本身去修改前驅節點的狀態。 */
			unparkSuccessor(node);
		}

		node.next = node; // help GC
	}
}
複製代碼

節點取消獲取鎖的狀況有兩種須要考慮:

  1. 當前節點是tail。
  2. 當前節點是中間節點。

第一種狀況處理就簡單了,直接將tail指向當前節點的prev,而後prev的next置空,當前節點就出隊了。 第二種狀況就比較複雜,若是當前節點的前驅節點不是head的話,那麼就必須將前驅節點的waitStatus設爲SIGNAL(-1),而後將前驅節點的next指向當前節點的next,當前節點的next的prev,指向前驅節點。 在這裏插入圖片描述 若是說當前節點的前驅節點是head,那麼就直接喚醒當前節點的後繼節點,由於老三變老二了,它有資格去競爭鎖了。

若是CAS修改節點的指向失敗了,也不要緊,喚醒當前節點的後繼節點,讓它本身去修改前驅節點的waitStatus,當前節點能夠安心出隊。

很顯然,線程B是不會觸發cancelAcquire()方法的。

假設,此時線程A依然沒有unlock,此時線程C也要來獲取鎖。顯然線程C會競爭失敗,AQS會將其封裝爲Node節點入隊,並將線程B的Node節點的waitStatus改成SIGNAL(-1),而後Park休眠。 此時,AQS的內部結構是: 在這裏插入圖片描述


ReentrantLock.unlock()作了什麼?

線程B、C成功入隊並Park後,假設此時線程A執行unlock釋放鎖: 在這裏插入圖片描述 釋放鎖的過程,實際上是調用了sync的release(),這也是AQS的模板方法:

// 釋放鎖
public final boolean release(int arg) {
	/* 調用子類的tryRelease(),返回true表明成功釋放鎖。 對於ReentrantLock來講,state減小至0表明須要釋放鎖。 */
	if (tryRelease(arg)) {
		/* head就表明持有鎖的節點。 若是head的waitStatus!=0,說明有後繼節點在等待被其喚醒。 還記得線程入隊時,若是要掛起,必須將其前驅節點的waitStatus改成-1嗎??? 若是節點入隊不改前驅節點的waitStatus,它將沒法被喚醒。 */
		Node h = head;
		if (h != null && h.waitStatus != 0)
			// 釋放鎖後要去喚醒後繼節點
			unparkSuccessor(h);
		return true;
	}
	return false;
}
複製代碼

AQS會調用子類實現的tryRelease(),當它返回true就表明成功釋放了資源,AQS就會去喚醒隊列中的節點。

/* 嘗試釋放鎖,返回true表明鎖成功釋放。 只有持有鎖的線程才能釋放鎖,所以不存在併發問題。 */
protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
	// 不是持有鎖的線程執行釋放鎖,拋異常。
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		// 當state==0才須要真正的釋放鎖
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}
複製代碼

ReentrantLock是可重入鎖,state表明鎖的重入次數,tryRelease()就是state自減的過程。當state減至0就表明鎖成功釋放了,同時會將exclusiveOwnerThread置空。

必須是持有鎖的線程才能調用tryRelease(),不然會拋異常。

線程A執行完tryRelease()後,此時AQS的內部結構是: 在這裏插入圖片描述

tryRelease()返回true,AQS就要去喚醒隊列中的節點了,執行unparkSuccessor()

// 喚醒後繼節點
private void unparkSuccessor(Node node) {
	// 將waitStatus置爲0
	int ws = node.waitStatus;
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0);

	/* 若是後繼節點的waitStatus>0,表明節點是CANCELLED的無效節點,會跳過。 而後從尾巴開始向頭找,直到找到waitStatus <= 0的有效節點,並將其喚醒。 爲何要從尾巴找? 是由於節點入隊時,須要執行三個操做: 1.當前節點的prev指向前任tail 2.CAS將tail指向當前節點 3.前任tail的next指向當前節點 若是執行到步驟2時,時間片到期,此時前驅節點的next仍是null,會存在漏喚醒的問題。 而prev的賦值操做先於CAS執行,所以經過prev向前找總能找到。 */
	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);
}
複製代碼

這裏有一個頗有意思的操做,當next節點無效時,AQS會跳過它,從新尋找有效的節點。AQS會從tail開始向head找,而不是從head向tail找,這是爲何呢???

從尾部向頭部找,是由於節點入隊時,須要執行三個操做:1.當前節點的prev指向前任tail、2.CAS將tail指向當前節點、3.前任tail的next指向當前節點。若是執行到步驟2時,時間片到期,此時前驅節點的next仍是null,會存在漏喚醒的問題。而prev的賦值操做先於CAS執行,所以經過prev向前找總能找到。

當節點被喚醒時,會從AQS的parkAndCheckInterrupt()方法裏繼續執行,從新獲取鎖。

線程A釋放鎖,並將線程B喚醒,線程B會繼續去競爭。 在這裏插入圖片描述 此時線程B會競爭成功,同時會將head指向當前節點。 此時AQS內部結構是: 在這裏插入圖片描述 線程B運行一段時間後,也釋放鎖了,接着會喚醒線程C。 在這裏插入圖片描述 線程C會成功獲取到鎖: 在這裏插入圖片描述 線程C釋放鎖後,最後一個Node節點並不會出列,而是會保留。當下一次有線程來競爭鎖時,成功後會自動將前任head覆蓋。


問題

最後再總結一下關於AQS幾個比較重要的問題。

1.工做線程何時出隊?

FIFO隊列中的一個節點競爭到資源時,它並非就立刻出隊了,而是將head指向本身。節點釋放鎖後依然不會主動出隊,而是等待下一個節點競爭鎖成功後修改head的指向,將前任head踢出去。

2.AQS喚醒隊列的規則是什麼?

head指向的節點成功釋放資源後,首先會判斷當前節點的waitStatus是否等於0,若是等於0就不會去喚醒後繼節點了,這也就是爲何新的節點入隊休眠的前提是必須將前驅節點的waitStatus改成SIGNAL(-1)的緣由,若是不改,後繼節點將不會被喚醒,就會致使死鎖。

AQS首先會喚醒當前節點的直接後繼節點next,若是next爲null,有兩種狀況:

  1. 確實沒有後繼節點了,以前next指向的節點可能因爲超時等緣由退出競爭了。
  2. 存在後繼節點,只是因爲多線程的緣由,後繼節點還沒來得及將當前節點的next指向它。

第一種狀況好辦,後繼節點爲null,不喚醒就是了。 第二種狀況就須要從tail向head尋找了,找到了有效節點再喚醒。

若是存在直接後繼節點,可是節點的waitStatus大於0,AQS也是會選擇跳過它的。前面已經說過,waitStatus大於0的節點表明無效節點,如CANCELLED(1)是已經取消競爭的節點。若是直接後繼節點是無效節點的話,AQS會從tail開始向head遍歷,直到找到有效節點,再將其喚醒。

總結:存在直接後繼節點且節點有效,則優先喚醒後繼節點。不然,從tail向head遍歷,直到找到有效節點再喚醒。

3.喚醒節點爲何要從尾巴開始?

這是由於,新節點入隊時,須要執行三個步驟:

  1. 當前節點的prev指向前任tail
  2. CAS將tail指向當前節點
  3. 前任tail的next指向當前節點

這三個操做AQS並無作同步處理,若是在執行步驟2後CPU時間片到期了,此時的節點指向是這樣的: 在這裏插入圖片描述 前驅節點的next尚未賦值,若是從頭向尾找,就可能會存在漏喚醒的問題。 而prev的賦值先於tail的CAS操做以前執行,所以從尾向頭找,就能夠避免這個問題。

4.其餘問題

佔個坑,問題能夠持續更新,想到了再更。有疑惑的同窗能夠評論告訴我,我會把我知道的記錄下來,你們一塊兒再探討一下。


尾巴

到如今還很印象深入,年初的時候有一次面試,就被問到AQS,當時沒有靜下心來研究,有些概念仍是很模糊,答得不是很好,面試官也不太滿意。

剛好上週加班,今天調休,能夠寫篇文章放鬆一下。因而我決定挑戰一下個人軟肋AQS,靜下心來閱讀源碼才發現,過去看網上的博客,一直比較模糊的概念,其實代碼裏已經寫的很是清楚了。

過去讀不懂的源碼,日後慢慢都會讀懂!!!


你可能感興趣的文章:

相關文章
相關標籤/搜索