JUC知識點總結(三)ReentrantLock與ReentrantReadWriteLock源碼解析

8. Lock接口 (ReentrantLock 可重入鎖)

特性

ReentantLock 繼承接口 Lock 並實現了接口中定義的方法, 它是一種可重入鎖, 除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等
避免多線程死鎖的方法。

  • 嘗試非阻塞地獲取鎖:tryLock(),調用方法後立刻返回;
  • 能被中斷地獲取鎖:lockInterruptibly():在鎖的獲取中可以中斷當前線程
  • 超時獲取鎖:tryLock(time,unit),超時返回

Condition 類和 Object 類鎖方法區別區別

  1. Condition 類的 awiat 方法和 Object 類的 wait 方法等效
  2. Condition 類的 signal 方法和 Object 類的 notify 方法等效
  3. Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
  4. ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的

tryLock 和 lock 和 lockInterruptibly 的區別

  1. tryLock 能獲得鎖就返回 true,不能就立即返回 false, tryLock(long timeout, TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false
  2. lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖
  3. lock 和 lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程,lock 不會拋出異常,而 lockInterruptibly 會拋出異常。

與Synchronized區別

  • ReentrantLock 通過方法 lock()與 unlock()來進行加鎖與解鎖操作,與 synchronized 會
    被 JVM 自動解鎖機制不同, ReentrantLock 加鎖後需要手動進行解鎖。爲了避免程序出
    現異常而無法正常解鎖的情況,使用 ReentrantLock 必須在 finally 控制塊中進行解鎖操
    作。
  • ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。這種情況下需要
    使用 ReentrantLock。

代碼示例

public class MyService {
	private Lock lock = new ReentrantLock();
	//Lock lock=new ReentrantLock(true);//公平鎖
	//Lock lock=new ReentrantLock(false);//非公平鎖
	private Condition condition=lock.newCondition();//創建 Condition
	public void testMethod() {
		try {
			lock.lock();//lock 加鎖
			//1: wait 方法等待:
			//System.out.println("開始 wait");
			condition.await();
			//通過創建 Condition 對象來使線程 wait,必須先執行 lock.lock 方法獲得鎖
			//:2: signal 方法喚醒
			condition.signal();//condition 對象的 signal 方法可以喚醒 wait 線程
			for (int i = 0; i < 5; i++) {
				System.out.println("ThreadName=" +
				Thread.currentThread().getName()+ (" " + (i + 1)));
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
}

ReentrantLock源碼分析

ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態。它支持公平鎖和非公平鎖,兩者的實現類似。AQS使用一個FIFO的隊列表示排隊等待鎖的線程,隊列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何線程關聯。其他的節點與等待線程關聯,每個節點維護一個等待狀態waitStatus。

ReentrantLock的基本實現可以概括爲:先通過CAS嘗試獲取鎖。如果此時已經有線程佔據了鎖,那就加入AQS隊列並且被掛起。當鎖被釋放後,排在CLH隊列隊首的線程會被喚醒,然後CAS再次嘗試獲取鎖。

參考併發編程——詳解 AQS CLH 鎖

非公平鎖NonfairSync lock()的過程
final void lock() {
	if (compareAndSetState(0, 1))//CAS操作,若state爲0則將其設爲1
    	setExclusiveOwnerThread(Thread.currentThread());
    else
    	acquire(1);
}
獲取鎖失敗進入acquire(1):
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}
tryAcquire(arg):第一步:嘗試去獲取鎖。
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();//獲取state變量值
	if (c == 0) { //沒有線程佔用鎖 :非公平鎖的特點
		if (compareAndSetState(0, acquires)) {//佔用鎖成功
			setExclusiveOwnerThread(current);//設置獨佔線程爲當前線程
			return true;
		}
	} else if (current == getExclusiveOwnerThread()) { //當前線程已經佔用該鎖
		int nextc = c + acquires;
		if (nextc < 0) // overflow
		throw new Error("Maximum lock count exceeded");    
		setState(nextc); // 更新state值爲新的重入次數
		return true;
	}  
	return false; //獲取鎖失敗
}

非公平鎖tryAcquire的流程是:檢查state字段,若爲0,表示鎖未被佔用,那麼嘗試佔用,若不爲0,檢查當前鎖是否被自己佔用,若被自己佔用,則更新state字段,表示重入鎖的次數。如果以上兩點都沒有成功,則獲取鎖失敗,返回false。

“非公平”即體現在這裏,如果佔用鎖的線程剛釋放鎖,state爲0,而排隊等待鎖的線程還未喚醒時,新來的線程就直接搶佔了該鎖,那麼就“插隊”了。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 第二步:獲取鎖失敗則入隊。

addWaiter(Node.EXCLUSIVE)將新節點和當前線程關聯並且入隊列:

private Node addWaiter(Node mode) {
 	//初始化節點,設置關聯線程和模式(獨佔 or 共享)
	Node node = new Node(Thread.currentThread(), mode);  
	Node pred = tail; // 獲取尾節點引用  
	if (pred != null) {// 尾節點不爲空,說明隊列已經初始化過
		node.prev = pred;    
		if (compareAndSetTail(pred, node)) {//CAS,設置新節點爲尾節點
			pred.next = node;
			return node;
		}
	}  
	enq(node); // 尾節點爲空,說明隊列還未初始化
	return node;
}

private Node enq(final Node node) {  
	for (;;) {//開始自旋
		Node t = tail;
		if (t == null) { // 如果tail爲空
			if (compareAndSetHead(new Node()))//新建一個head節點
				tail = head; //tail指向head
		} else {
			node.prev = t;      
			if (compareAndSetTail(t, node)) {// tail不爲空
				t.next = node; //將新節點入隊
				return t;
			}
		}
	}
}
acquireQueued(final Node node, int arg) 已經入隊的線程嘗試獲取鎖,若失敗則會被掛起。
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true; //標記是否成功獲取鎖
	try {
		boolean interrupted = false; //標記線程是否被中斷過
		for (;;) {
			final Node p = node.predecessor(); //獲取前驅節點
			//如果前驅是head,即該結點已成老二,那麼便有資格去嘗試獲取鎖
			if (p == head && tryAcquire(arg)) {
				setHead(node); // 獲取成功,將當前節點設置爲head節點
				p.next = null; // 原head節點出隊,在某個時間點被GC
				failed = false; //獲取成功
				return interrupted; //返回是否被中斷過
			}
			// 判斷獲取失敗後是否可以掛起,若可以則掛起
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				// 線程若被中斷,設置interrupted爲true
				interrupted = true;
		}
	} finally {
		if (failed)
		cancelAcquire(node);
	}
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//前驅節點的狀態
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 前驅節點狀態爲signal,返回true
		return true;
		// 前驅節點狀態爲CANCELLED
		if (ws > 0) {
		// 從隊尾向前尋找第一個狀態不爲CANCELLED的節點
			do {
				node.prev = pred = pred.prev;
			} while (pred.waitStatus > 0);
				pred.next = node;
			} else {
		// 將前驅節點的狀態設置爲SIGNAL
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);// 掛起當前線程,返回線程中斷狀態並重置
	return Thread.interrupted();
}

​ 線程入隊後能夠掛起的前提是,它的前驅節點的狀態爲SIGNAL,它的含義是“Hi,前面的兄弟,如果你獲取鎖並且出隊後,記得把我喚醒!”。所以shouldParkAfterFailedAcquire會先判斷當前節點的前驅是否狀態符合要求,若符合則返回true,然後調用parkAndCheckInterrupt,將自己掛起。如果不符合,再看前驅節點是否>0(CANCELLED),若是那麼向前遍歷直到找到第一個符合要求的前驅,若不是則將前驅節點的狀態設置爲SIGNAL。整個流程中,如果前驅結點的狀態不是SIGNAL,那麼自己就不能安心掛起,需要去找個安心的掛起點,同時可以再嘗試下看有沒有機會去嘗試競爭鎖。

非公平鎖NonfairSync unlock()的過程:
public void unlock() {
	sync.release(1);
}

public final boolean release(int arg) {
	if (tryRelease(arg)) {//嘗試釋放鎖
		Node h = head;
		if (h != null && h.waitStatus != 0)//若頭結點的狀態是SIGNAL
			unparkSuccessor(h);//喚醒頭結點下一個節點的關聯線程
		return true;
	}
	return false;
}

protected final boolean tryRelease(int releases) {
	int c = getState() - releases; // 計算釋放後state值
	// 如果不是當前線程佔用鎖,那麼拋出異常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {    
		free = true; // 鎖被重入次數爲0,表示釋放成功    
		setExclusiveOwnerThread(null); // 清空獨佔線程
	}  
	setState(c); // 更新state值
	return free;
}

tryRelease的過程爲:當前釋放鎖的線程若不持有鎖,則拋出異常。若持有鎖,計算釋放後的state值是否爲0,若爲0表示鎖已經被成功釋放,並且則清空獨佔線程,最後更新state值,返回free。

公平鎖和非公平鎖

公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state

公平鎖獲取時,首先會去讀volatile變量,若爲0,按隊列順序獲取鎖

非公平鎖獲取時,首先會用CAS更新volatile變量,若爲0,當前線程可直接搶佔

tryLock():線程獲取鎖失敗後,先入等待隊列,然後開始自旋,嘗試獲取鎖,獲取成功就返回,失敗則在隊列裏找一個安全點把自己掛起直到超時時間過期。這裏爲什麼還需要循環呢?因爲當前線程節點的前驅狀態可能不是SIGNAL,那麼在當前這一輪循環中線程不會被掛起,然後更新超時時間,開始新一輪的嘗試。

ReentrantReadWriteLock 源碼分析

ReentrantReadWriteLock包含兩個內部類: ReadLock和WriteLock,獲取鎖和釋放鎖都是通過AQS來實現的。AQS的狀態state是32位的,讀鎖用高16位,表示持有讀鎖的線程數(sharedCount),寫鎖低16位,表示寫鎖的重入次數(exclusiveCount)。

線程進入讀鎖的前提條件:(共享鎖)
  • 沒有其他線程的擁有寫鎖,
  • 沒有寫請求或者有寫請求,但調用線程和持有讀鎖的線程是同一個。
線程進入寫鎖的前提條件:(排他鎖/獨佔鎖)
  • 沒有其他線程的讀鎖
  • 沒有其他線程的寫鎖
讀寫鎖有以下三個重要的特性:
  • 公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
  • 重進入:讀鎖和寫鎖都支持線程重進入。
  • 鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖。
獲取寫鎖的步驟:

(1)判斷同步狀態state是否爲0。如果state!=0,說明已經有其他線程獲取鎖,執行(2);否則執行(5)。

(2)若讀鎖此時被其他線程佔用,或其他線程獲取寫鎖,則返回false,當前線程不能獲取寫鎖。

(3)若當前線程獲取寫鎖超過最大次數,拋異常,否則更新同步狀態,返回true。

(4)如果state爲0,此時讀鎖或寫鎖都沒有被獲取,判斷是否需要阻塞(公平和非公平方式實現不同),在非公平策略下總是不會被阻塞,在公平策略下會進行判斷(判斷同步隊列中是否有等待時間更長的線程,若存在,則需要被阻塞,否則,無需阻塞),如果不需要阻塞,則CAS更新同步狀態,若CAS成功則返回true,失敗則說明鎖被別的線程搶去了,返回false。如果需要阻塞則也返回false。

(5)成功獲取寫鎖後,將當前線程設置爲佔有寫鎖的線程,返回true。

釋放寫鎖的步驟:

(1)查看當前線程是否爲寫鎖的持有者,如果不是拋出異常。

(2)檢查釋放後寫鎖的線程數是否爲0,如果爲0則表示寫鎖空閒了,釋放鎖資源將鎖的持有線程設置爲null,否則釋放僅僅只是一次重入鎖而已,並不能將寫鎖的線程清空。

獲取讀鎖的步驟:

(1)若寫鎖線程數 != 0 ,且獨佔鎖不是當前線程,則返回失敗

(2)否則,判斷讀線程是否需要被阻塞並且讀鎖數量是否小於最大值並且CAS設置狀態

(3)若當前沒有讀鎖,則設置第一個讀線程firstReader和firstReaderHoldCount;若當前線程線程就是第一個讀線程,則爲重入增加firstReaderHoldCount;否則,將設置當前線程對應的HoldCounter對象的值。

釋放讀鎖的步驟:

(1)判斷當前線程是否爲第一個讀線程firstReader,若是,則判斷第一個讀線程佔有的資源數firstReaderHoldCount是否爲1,若是,則設置第一個讀線程firstReader爲空,否則,將第一個讀線程佔有的資源數firstReaderHoldCount減1;

(2)若當前線程不是第一個讀線程,那麼首先會獲取緩存計數器(上一個讀鎖線程對應的計數器),若計數器爲空或者tid不等於當前線程的tid值,則獲取當前線程的計數器,如果計數器的計數count小於等於1,則移除當前線程對應的計數器,如果計數器的計數count小於等於0,則拋出異常,之後再減少計數即可。無論何種情況,都會進入無限循環,該循環可以確保成功設置狀態state。

總結:

在線程持有讀鎖的情況下,該線程不能取得寫鎖(因爲獲取寫鎖的時候,如果發現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前線程持有)。

在線程持有寫鎖的情況下,該線程可以繼續獲取讀鎖(獲取讀鎖時如果發現寫鎖被佔用,只有寫鎖沒有被當前線程佔用的情況纔會獲取失敗)。

寫鎖可以“降級”爲讀鎖;讀鎖不能“升級”爲寫鎖。

下一篇
JUC知識點總結(四)五種單例模式的寫法

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