8. Lock接口 (ReentrantLock 可重入鎖)
特性
ReentantLock 繼承接口 Lock 並實現了接口中定義的方法, 它是一種可重入鎖, 除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等
避免多線程死鎖的方法。
- 嘗試非阻塞地獲取鎖:tryLock(),調用方法後立刻返回;
- 能被中斷地獲取鎖:lockInterruptibly():在鎖的獲取中可以中斷當前線程
- 超時獲取鎖:tryLock(time,unit),超時返回
Condition 類和 Object 類鎖方法區別區別
- Condition 類的 awiat 方法和 Object 類的 wait 方法等效
- Condition 類的 signal 方法和 Object 類的 notify 方法等效
- Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
- ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的
tryLock 和 lock 和 lockInterruptibly 的區別
- tryLock 能獲得鎖就返回 true,不能就立即返回 false, tryLock(long timeout, TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false
- lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖
- 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再次嘗試獲取鎖。
非公平鎖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。
總結:
在線程持有讀鎖的情況下,該線程不能取得寫鎖(因爲獲取寫鎖的時候,如果發現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前線程持有)。
在線程持有寫鎖的情況下,該線程可以繼續獲取讀鎖(獲取讀鎖時如果發現寫鎖被佔用,只有寫鎖沒有被當前線程佔用的情況纔會獲取失敗)。
寫鎖可以“降級”爲讀鎖;讀鎖不能“升級”爲寫鎖。