java 並發(六) --- 鎖
閱讀前閱讀以下參考資料,文章圖片或代碼部分來自與參考資料
概覽
一張圖了解一下java鎖.
各種鎖
為什麽要設置鎖的等級
jdk1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。註意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
樂觀鎖和悲觀鎖
閱讀這篇文章,了解樂觀鎖和悲觀鎖 ---- 不可不說的Java“鎖”事
我們可以知道 :
- 樂觀鎖與悲觀鎖的區分在於有沒有對同步資源上鎖.
- 樂觀鎖沒有上鎖,保證線程同步的常用實現是 CAS
- 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
- 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
自旋鎖和自適應自旋鎖
解決的通點 :
在介紹自旋鎖前,我們需要介紹一些前提知識來幫助大家明白自旋鎖的概念。
阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。
在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。如果物理機器有多個處理器,能夠讓兩個或以上的線程同時並行執行,我們就可以讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。
而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麽當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
自適應鎖
JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎麽做呢?線程如果自旋成功了,那麽下次自旋的次數會更加多,因為虛擬機認為既然上次成功了,那麽此次自旋也很有可能會再次成功,那麽它就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋能夠成功的,那麽在以後要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。
有了自適應自旋鎖,隨著程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測會越來越準確,虛擬機會變得越來越聰明。
即是說這次成功獲取到鎖,那麽就意味著該線程獲取到鎖的幾率比較大,那麽運行自旋的次數就會多些.
在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,本文中僅做名詞介紹,不做深入講解,感興趣的同學可以自行查閱相關資料。
無鎖 , 偏向鎖 ,輕量級鎖 ,重量級鎖
這四種鎖是指鎖的狀態,專門針對synchronized的。在介紹這四種鎖狀態之前還需要介紹一些額外的知識。首先為什麽Synchronized能實現線程同步?在回答這個問題之前我們需要了解兩個重要的概念:“Java對象頭”、“Monitor”。
Java 對象頭
下面內容引用來自 【死磕Java並發】—–深入分析synchronized的實現原理
synchronized用的鎖是存在Java對象頭裏的,那麽什麽是Java對象頭呢?Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。其中Klass Point是是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵,所以下面將重點闡述
Mark Word
Mark Word用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit),但是如果對象是數組類型,則需要三個機器碼,因為JVM虛擬機可以通過Java對象的元數據信息確定Java對象的大小,但是無法從數組的元數據來確認數組的大小,所以用一塊來記錄數組長度。下圖是Java對象頭的存儲結構(32位虛擬機):
對象頭信息是與對象自身定義的數據無關的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據,它會根據對象的狀態復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發生變化,變化狀態如下(32位虛擬機):
總結一下java頭對象信息
Monitor
什麽是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個對象。
與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java對象自打娘胎裏出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。其結構如下:
- Owner:初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程唯一標識,當鎖被釋放時又設置為NULL;
- EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
- RcThis:表示blocked或waiting在該monitor record上的所有線程的個數。
- Nest:用來實現重入鎖的計數。
- HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
- Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
有了這種結構我們就可以實現自旋了,因為Monitor的數據結構可以讓其他線程判斷鎖是否釋放.
無鎖
無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。
無鎖的特點就是修改操作在循環內進行,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功並退出,否則就會繼續循環嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。上面我們介紹的CAS原理及應用即是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。
輕量級鎖
引入輕量級鎖的主要目的是在多沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下:
獲取鎖
- 判斷當前對象是否處於無鎖狀態(hashcode、0、01),若是,則JVM首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word);否則執行步驟(3);
- JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖,則將鎖標誌位變成00(表示此對象處於輕量級鎖狀態),執行同步操作;如果失敗則執行步驟(3);
- 判斷當前對象的Mark Word是否指向當前線程的棧幀,如果是則表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊;否則只能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖標誌位變成10,後面等待的線程將會進入阻塞狀態;
釋放鎖
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
- 取出在獲取輕量級鎖保存在Displaced Mark Word中的數據;
- 用CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功,否則執行(3);
- 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程。
對於輕量級鎖,其性能提升的依據是“對於絕大部分的鎖,在整個生命周期內都是不會存在競爭的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢;
下圖是輕量級鎖的獲取和釋放過程
偏向鎖
引入偏向鎖主要目的是:為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。上面提到了輕量級鎖的加鎖解鎖操作
是需要依賴多次CAS原子指令的。那麽偏向鎖是如何來減少不必要的CAS操作呢?我們可以查看Mark work的結構就明白了。只需要檢查
是否為偏向鎖、鎖標識為以及ThreadID (標識)即可,處理流程如下:
獲取鎖
- 檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01;
- 若為可偏向狀態,則測試線程ID是否為當前線程ID,如果是,則執行步驟(5),否則執行步驟(3);
- 如果線程ID不為當前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當前線程ID,否則執行步驟(4);
- 通過CAS競爭鎖失敗,證明當前存在多線程競爭情況,當到達全局安全點(在這個時間點上沒有字節碼正在執行),獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼塊;
- 執行同步代碼塊
可以看到在步驟3的時候,要是成功,只會發生一次CAS 交換.同時我們也可以看到,隨著競爭的激烈程度,會導致鎖的層次逐漸上升上去.
偏向鎖在JDK 6及以後的JVM裏是默認啟用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程序默認會進入輕量級鎖狀態。
釋放鎖
偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟如下:
- 暫停擁有偏向鎖的線程,判斷鎖對象石是否還處於被鎖定狀態;
- 撤銷偏向蘇,恢復到無鎖狀態(01)或者輕量級鎖的狀態;
下圖是偏向鎖的獲取和釋放流程
重量級鎖
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。
鎖的流程如下 :
公平鎖 和 非公平鎖
ReentrantLock 內部實現了公平鎖和非公平鎖。
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麽這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。
下面的圖可以解釋這兩種的差別 :
上圖.公平鎖 下圖.非公平鎖
接下來看看具體的源碼實現
ReentranLock 和 Synchronized 的區別
- reentranLock 可重入,同時公平鎖和非公平鎖和Synchronized 獲取鎖的方式對比,具有性能優勢(非公平鎖吞吐量大)。
- synchronized與Lock在默認情況下是不會響應中斷(interrupt)操作,會繼續執行完。lockInterruptibly()提供了可中斷鎖來解決此問題。
鎖優化
鎖消除和鎖粗化
這方面的內容請看 【死磕Java並發】—–深入分析synchronized的實現原理
參考資料
- 【死磕Java並發】—–深入分析synchronized的實現原理
- 不可不說的Java“鎖”事
java 並發(六) --- 鎖