面試官提問
● 公平鎖與非公平鎖的區別是什么?
● 什么是可重入鎖?
● 什么是死鎖,怎樣避免死鎖?
● ReentrantLock與Syschronized實現原理是什么?兩者有什么區別?
● 請說明ReentrantLock獲取鎖與釋放鎖的流程。
● 請說明Syschronized鎖升級的過程。
● 鎖性能優化方法是什么?
● 介紹一下AbstractQueuedSynchronizer(AQS)。
1 公平鎖與非公平鎖
公平鎖是指多個線程競爭鎖時直接進入隊列排隊,根據申請鎖的順序獲得鎖,先到先得。而非公平鎖則是多個線程競爭鎖時,首先嘗試直接搶鎖,失敗后再進入等待隊列。
使用公平鎖,先到先得,線程獲取鎖時不會出現饑餓現象。使用非公平鎖,整體的吞吐效率比較高。
ReentrantLock默認是非公平鎖,在構造方法中傳入參數true則為公平鎖;Synchronized是非公平鎖。
2 可重入鎖
可重入鎖是指一個線程可以多次獲取同一把鎖,其實現原理是,為每個鎖關聯一個計數器,線程首次獲取鎖時,計數器置為1,再次獲取該鎖時,計數器加1;線程每退出同步塊一次,計數器就減1。計數器為0則代表鎖被當前線程釋放。
Synchronized和ReentrantLock都是可重入鎖。
3 ReentrantLock鎖
ReentrantLock鎖的特點是可重入,支持公平鎖和非公平鎖兩種方式。
閱讀ReentrantLock代碼可知,它主要利用CAS+AQS隊列來實現。以非公平鎖為例,當線程競爭鎖時首先使用CAS搶占鎖,成功則返回,失敗則進入AQS隊列并且掛起線程;當鎖被釋放時,喚醒AQS中的某個線程,從被掛起處再次嘗試獲取鎖(當AQS隊列頭節點的下一個節點不為空時,直接喚醒該節點;否則從隊尾向前遍歷,找到最后一個不為空的節點并喚醒),獲取鎖失敗則再次進入隊尾。圖1-13詳細描述了ReentrantLock非公平鎖的獲取與釋放流程。
下面通過源碼來分析ReentrantLock的實現。非公平鎖首先使用CAS檢測鎖是否空閑并搶占鎖,當多個線程同時搶占同一把鎖時,CAS操作保證只有一個線程執行成功。
final void lock() {//state為0則計數器設為1,表示搶占鎖成功if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
假設3個線程T1、T2和T3同時競爭鎖,線程T1執行CAS成功,線程T2和T3則會進入acquire方法:
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
接下來分別閱讀tryAcquire、addWaiter和acquireQueued的實現代碼。
進入tryAcquire方法,若鎖空閑(state = 0),則當前線程通過CAS直接搶鎖,搶鎖成功則返回true;搶鎖失敗則判斷持有鎖的線程是否為自己,如果是自己的話就記錄重入鎖的次數,并返回獲取鎖成功,否則返回獲取鎖失敗。
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();//鎖處于空閑狀態,沒有被任何線程持有if (c == 0) {//忽略AQS隊列中的等待線程,當前線程直接通過CAS搶鎖體現了非公平性if (compareAndSetState(0, acquires)) {//搶鎖成功,設置獨占線程為當前線程setExclusiveOwnerThread(current);return true;}}//檢查持有鎖的線程是否為當前線程else if (current == getExclusiveOwnerThread()) {//可重入鎖,記錄重入次數int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}//獲取鎖失敗return false;}
若tryAcquie獲取鎖失敗,則執行addWaiter方法,線程加入AQS隊列尾部,具體代碼如下:
private Node addWaiter(Node mode) {//初始化節點,模式設置為獨占Node node = new Node(Thread.currentThread(), mode);Node pred = tail;//tail不為null,說明隊列已被初始化if (pred != null) {node.prev = pred;//通過CAS將Node對象加入AQS隊列,成為尾節點if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//隊列未初始化或者CAS操作失敗則進入enq函數enq(node);return node;}
T2和T3線程搶鎖失敗,假設它們同時加入AQS隊列,由于隊列尚未初始化(tail ==null),因此至少有一個線程進入enq()方法,代碼如下:
這段代碼通過自旋和CAS來實現非阻塞的原子操作,保證線程安全。假設T2和T3線程同時執行enq方法,**第一輪循環,CAS操作確保只有一個線程創建head節點;**第二輪循環,AQS隊列完成初始化,tail非空,T2和T3線程都進入else邏輯,通過CAS操作將當前節點加入隊尾。若T2線程執行compareAndSetTail成功,則T3線程需要在下一次循環時入隊,最終AQS隊列如圖1-14所示。
T2和T3線程進入隊列后執行acquireQueued()方法,AQS隊列頭節點的后繼節點可以再次嘗試獲取鎖,獲取鎖失敗后被掛起,代碼如下:
如果T1線程一直持有鎖,那么T2和T3線程最終會進入shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法,代碼如下:
最終T2和T3線程在狀態為Node.SIGNAL的前驅節點后掛起,保證前驅節點獲取鎖后能喚醒自己。AQS隊列中節點的狀態及說明如表1-1所示。
鎖的釋放過程比較簡單,代碼如下:
public void unlock() {sync.release(1);}public final boolean release(int arg) {if (tryRelease(arg)) {//釋放鎖成功Node h = head;//喚醒AQS隊列中的某個節點(一般是頭節點)if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}
核心方法是tryRelease和unparkSuccessor,先看一下tryRelease的執行過程,代碼如下:
protected final boolean tryRelease(int releases) {//重入鎖,每重入一次則state加1,每釋放鎖一次則state 減1int c = getState() - releases;// 若當前線程不是持有鎖的線程則拋出異常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//state 減為 0,代表釋放鎖成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
釋放鎖成功后會喚起AQS隊列中被掛起的線程,代碼如下:
private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {// 如果節點為null或者處于取消狀態// 那就從后往前遍歷尋找距離頭節點最近的非取消節點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);
}
被喚醒的線程也不能保證搶鎖成功,失敗后依然會放置在隊尾,這里也體現了鎖的“非公平”性。
4 Syschronized鎖
在HotSpot虛擬機中,對象內存布局主要分為對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding),如圖1-15所示。
當線程訪問同步塊時,首先需要獲得鎖并把相關信息存儲在對象頭中,對象頭由以下兩部分組成:
● Mark Word:存儲自身運行時數據,例如HashCode、GC年齡、鎖相關信息等內容。MarkWord信息結構如表1-2所示。
● Klass Pointer:Class對象的類型指針,指向的位置是對象對應的Class對象(對應的元數據對象)的內存地址。
總體上來說,鎖升級過程如圖1-16所示。
1)偏向鎖
線程獲取偏向鎖的流程如下:
● 檢查Mark Word中的線程id。
● 若線程id為空,則通過CAS設置為當前線程id:成功則獲取鎖,失敗則撤銷偏向鎖。
● 若線程id不為空且為當前線程,則獲取鎖成功,否則撤銷偏向鎖
持有偏向鎖的線程每次進入這個鎖相關的同步塊時,只需判斷Mark Word中記錄的線程id是否為自己。在沒有競爭時,一個線程反復申請獲得同一把鎖,使用偏向鎖效率極高。
2)輕量級鎖
多個線程競爭偏向鎖導致鎖升級為輕量級鎖,獲取輕量級鎖的流程如下:
● 線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間LockRecord,并將對象頭中的Mark Word復制到Lock Record。
● 利用CAS操作將對象頭中的Mark Word更新為指向Lock Record的指針,若操作成功則競爭到鎖,鎖標志位變為00,表示當前為輕量級鎖狀 態。
● CAS執行失敗且自旋一定次數后仍未成功,則說明該鎖已被其他線程搶占,這時輕量級鎖會膨脹為重量級鎖,鎖標志位變成為10。
使用輕量級鎖提升性能的前提**:多個線程交替執行同步塊,鎖在整個生命周期內基本不會存在競爭或者出現鎖競爭的概率很低,減少了使用重量級鎖產生的性能消耗。**
輕量級鎖與偏向鎖的比較:輕量級鎖每次申請、釋放都至少需要一次CAS操作,但偏向鎖只有在初始化時需要一次CAS操作,后續當前線程可以幾乎零成本地直接獲得鎖(僅需比較線程id是否為自己)。
3)自旋鎖
如果持有鎖的線程能在很短時間內釋放鎖,那么競爭鎖的線程就沒有必要阻塞掛起,它們只需要自旋等待持有鎖的線程釋放鎖,然后再嘗試獲取鎖,這樣就能減少傳統的重量級鎖因使用操作系統互斥量而產生的性能開銷。因此,在輕量級鎖膨脹為重量級鎖之前,一般會嘗試通過自旋的方式獲取鎖。假如當前持有鎖的線程一直不釋放鎖,那么自旋就是在無意義地浪費CPU時間,當自旋多次始終無法獲取鎖時,輕量級鎖會膨脹為重量級鎖。
4)重量級鎖
沒有競爭到鎖的線程會被掛起,只有在持有鎖的線程退出同步塊之后才會喚醒這些線程。喚醒操作涉及操作系統調度,開銷很大。