java“鎖”事(2018美團點評技術文章合輯 閱讀筆記)

eluanshi12發表於2018-12-03

在這裡插入圖片描述

1.樂觀鎖 VS 悲觀鎖

悲觀鎖 認為自己在 使用資料的時候一定有別的執行緒來修改資料,因此在獲取資料的時候會先加鎖,確保資料不會被別的執行緒修改。(synchronized和Lock)
樂觀鎖 認為自己在 使用資料時不會有別的執行緒修改資料,所以不會新增鎖,只是在更新資料的時候去判斷之前有沒有別的執行緒更新了這個資料。如果這個資料沒有被更新,當前執行緒將自己修改的資料成功寫入。如果資料已經被其他執行緒更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。
在這裡插入圖片描述

  • 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時資料正確。
  • 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的效能大幅提升。
// ------------------------- 悲觀鎖的呼叫方式 -------------------------
// synchronized
public synchronized void testMethod() {
		// 操作同步資源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保證多個執行緒使用的是同一個鎖
public void modifyPublicResources() {
			lock.lock();
			// 操作同步資源
			lock.unlock();
}
// ------------------------- 樂觀鎖的呼叫方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保證多個執行緒使用的是同一個AtomicInteger
atomicInteger.incrementAndGet(); //執行自增1

CAS與Synchronized

從思想上來說:

  • Synchronized屬於悲觀鎖,悲觀地認為程式中的併發情況嚴重,所以嚴防死守。
  • CAS屬於樂觀鎖,樂觀地認為程式中的併發情況不那麼嚴重,所以讓執行緒不斷去嘗試更新。

2. 自旋鎖 VS 適應性自旋鎖

自旋鎖

阻塞或喚醒一個Java執行緒需要作業系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間(上下文切換耗時)。如果同步程式碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。
如果物理機器有多個處理器,能夠讓兩個或以上的執行緒同時並行執行,我們就可以讓後面那個請求鎖的執行緒不放棄CPU的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。

而為了讓當前執行緒“稍等一下”,我們需讓當前執行緒進行自旋,如果在自旋完成後前面鎖定同步資源的執行緒已經釋放了鎖,那麼當前執行緒就可以不必阻塞而是直接獲取同步資源,從而避免切換執行緒的開銷。這就是自旋鎖。

在這裡插入圖片描述
缺點:它不能代替阻塞。自旋等待雖然避免了執行緒切換的開銷,但它要佔用處理器時間。
所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(預設是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起執行緒。

自旋鎖的實現原理-CAS,AtomicInteger中呼叫unsafe進行自增操作的原始碼中的do-while迴圈就是一個自旋操作,如果修改數值失敗則通過迴圈來執行自旋,直至修改成功。

適應性自旋鎖

自適應意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在運行中,那麼虛擬機器就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。
如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞執行緒,避免浪費處理器資源。(TicketLock、CLHlock和MCSlock)。

3. 無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

java物件在記憶體中的結構(HotSpot虛擬機器)
鎖消除:JIT編譯時,對上下文進行掃描,去除不可能存在競爭的鎖。(StringBuffer 的StringBuffer ("…"))
鎖粗化:通過擴大鎖的範圍,避免反覆加鎖、解鎖(while迴圈中)
Java物件頭裡的Mark Word儲存結構
 Java物件頭的儲存結構
Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化
Mark Word會隨著程式的執行發生變化

偏向鎖

大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。
核心思想:如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word裡的結構變成偏向鎖結構,當該執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的執行緒只需要檢查Mark Word的鎖標記位為偏向鎖以及當前執行緒ID等於Mark Word的Thread ID即可,省去了大量有關鎖申請的操作。

全域性安全點(在這個時間點上沒有正
在執行的位元組碼)

輕量級鎖

偏向鎖執行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候升級為輕量級鎖。(執行緒交替執行同步塊)

重量級鎖

多執行緒同一時間訪問同一鎖的情況下,膨脹為重量級鎖。

鎖的優缺點對比

在這裡插入圖片描述
鎖狀態特點
四種鎖狀態特點
在這裡插入圖片描述
偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。
輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免執行緒阻塞和喚醒而影響效能。
重量級鎖是將除了擁有鎖的執行緒以外的執行緒都阻塞。

4.公平鎖 VS 非公平鎖

公平鎖
指多個執行緒按照申請鎖的順序來獲取鎖,執行緒直接進入隊列中排隊,隊列中的第一個執行緒才能獲得鎖。

  • 優點:等待鎖的執行緒不會餓死。
  • 缺點:整體吞吐效率相對非公平鎖要低,等待隊列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。
    公平鎖
    單視窗業務處理 取號後在座椅上等待, 叫號叫到(被喚醒)去視窗辦理業務

非公平鎖
多個執行緒加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個執行緒可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的執行緒先獲取鎖的場景。

  • 優點:可以減少喚起執行緒的開銷,整體的吞吐效率高,因為執行緒有機率不阻塞直接獲得鎖,CPU不必喚醒所有執行緒。
  • 缺點:處於等待隊列中的執行緒可能會餓死,或者等很久才會獲得鎖。
    非公平鎖的實現

ReentrantLock的原始碼:公平鎖和非公平鎖的實現。

5.可重入鎖 VS 非可重入鎖

可重入鎖又名遞迴鎖,是指在同一個執行緒在外層方法獲取鎖的時候,再進入該執行緒的內層方法會自動獲取鎖(前提鎖物件得是同一個物件或者class),不會因為之前已經獲取過還沒釋放而阻塞。
Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖

public class Widget {
	public synchronized void doSomething() {
		System.out.println("方法1執行...");
		doOthers();
	}
	public synchronized void doOthers() {
		System.out.println("方法2執行...");
	}
}

類中的兩個方法都是被內建鎖synchronized修飾的,doSomething()方法中呼叫doOthers()方法。因為內建鎖是可重入的,所以同一個執行緒在呼叫doOthers()時可以直接獲得當前物件的鎖,進入doOthers()進行操作。

如果是一個不可重入鎖,那麼當前執行緒在呼叫doOthers()之前需要將執行doSomething()時獲取當前物件的鎖釋放掉,實際上該物件鎖已被當前執行緒所持有,且無法釋放。所以此時會出現死鎖。
可重入鎖理解(一個村民帶多個水桶打水)

但如果是非可重入鎖的話,此時管理員只允許鎖和同一個人的一個水桶繫結。第一個水桶和鎖繫結打完水之後並不會釋放鎖,導致第二個水桶不能和鎖繫結也無法打水。當前執行緒出現死鎖,整個等待隊列中的所有執行緒都無法被喚醒。非可重入鎖

重入鎖ReentrantLock非可重入鎖NonReentrantLock

原始碼對比為什麼非可重入鎖在重複呼叫同步資源時會出現死鎖。
ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護了一個同步狀態status來計數重入次數,status初始值為0。

當執行緒嘗試獲取鎖時,可重入鎖先嚐試獲取並更新status值,如果status == 0表示沒有其他執行緒在執行同步程式碼,則把status置為1,當前執行緒開始執行。如果status != 0,則判斷當前執行緒是否是獲取到這個鎖的執行緒,如果是的話執行status+1,且當前執行緒可以再次獲取鎖。

而非可重入鎖是直接去獲取並嘗試更新當前status的值,如果status != 0的話會導致其獲取鎖失敗,當前執行緒阻塞。
原始碼
釋放鎖時,可重入鎖同樣先獲取當前status的值,在當前執行緒是持有鎖的執行緒的前提下。如果status-1== 0,則表示當前執行緒所有重複獲取鎖的操作都已經執行完畢,然後該執行緒才會真正釋放鎖。
而非可重入鎖則是在確定當前執行緒是持有鎖的執行緒之後,直接將status置為0,將鎖釋放。

6.獨享鎖 VS 共享鎖

獨享鎖也叫排他鎖,是指該鎖一次只能被一個執行緒所持有。 如果執行緒T對資料A加上排它鎖後,則其他執行緒不能再對A加任何型別的鎖。獲得排它鎖的執行緒即能讀資料又能修改資料
JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。

共享鎖是指該鎖可被多個執行緒所持有。 如果執行緒T對資料A加上共享鎖後,則其他執行緒只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的執行緒只能讀資料,不能修改資料
讀寫鎖ReentrantReadWriteLock
在ReentrantReadWriteLock里面,讀鎖ReadLock和寫鎖WriteLock的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨享鎖。
讀鎖的共享鎖可保證併發讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因為讀鎖和寫鎖是分離的。所以ReentrantReadWriteLock的併發性相比一般的互斥鎖有了很大提升。

在獨享鎖中state這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數),在共享鎖中state就是持有鎖的數量。
在這裡插入圖片描述
但是ReentrantReadWriteLock中有讀、寫兩把鎖,所以需要在一個整型變量state上分別描述讀鎖和寫鎖的數量(或者也可以叫狀態)。於是將state變量“按位切割”切分成了兩個部分,高16位表示讀鎖狀態(讀鎖個數),低16位表示寫鎖狀態(寫鎖個數)。

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

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

因為當執行緒獲取讀鎖的時候,可能有其他執行緒同時也在持有讀鎖,因此不能把獲取讀鎖的執行緒“升級”為寫鎖;
而對於獲得寫鎖的執行緒,它一定獨佔了讀寫鎖,因此可以繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖後,
還可以先釋放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”為了讀鎖。

綜上:
一個執行緒要想同時持有寫鎖和讀鎖,必須先獲取寫鎖再獲取讀鎖;
寫鎖可以“降級”為讀鎖;
讀鎖不能“升級”為寫鎖。

ReentrantLock無論讀操作還是寫操作,新增的鎖都是都是獨享鎖。

相關文章