多線程之——synchronized基本原理
提起synchronized大家都知道它是通過加鎖且是加了重鎖來實現(xiàn)線程安全,但是隨著JDK的發(fā)展,尤其在JDK1.6之后synchronized從原來的重鎖變得沒那么重了。下面我們將對synchronized進(jìn)行分析,看看JDK對它進(jìn)行了哪些優(yōu)化。
思考鎖是如何存儲的
可以思考一下,要實現(xiàn)多線程的互斥特性,那這把鎖需要哪些因素?
- 鎖需要有一個東西來表示,比如獲得鎖是什么狀態(tài)、無鎖狀態(tài)是什么狀態(tài)
- 這個狀態(tài)需要對多個線程共享
那么我們來分析,synchronized 鎖是如何存儲的呢?觀察synchronized 的整個語法發(fā)現(xiàn),synchronized(lock)是基于lock 這個對象的生命周期來控制鎖粒度的,那是不是鎖的存儲和這個 lock 對象有關(guān)系呢?于是我們以對象在 jvm 內(nèi)存中是如何存儲作為切入點(diǎn),去看看對象里面有什么特性能夠?qū)崿F(xiàn)鎖
對象在內(nèi)存中的布局
在 Hotspot 虛擬機(jī)中,對象在內(nèi)存中的存儲布局,可以分為三個區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)、對齊填充(Padding)

探究 Jvm 源碼實現(xiàn)
當(dāng)我們在 Java 代碼中,使用 new 創(chuàng)建一個對象實例的時候,(hotspot 虛擬機(jī))JVM 層面實際上會創(chuàng)建一個instanceOopDesc 對象。Hotspot 虛擬機(jī)采用 OOP-Klass 模型來描述 Java 對象實例,OOP(Ordinary Object Point)指的是普通對象指針,Klass 用來描述對象實例的具體類型。Hotspot 采用instanceOopDesc 和 arrayOopDesc 來 描述對象 頭,arrayOopDesc 對象用來描述數(shù)組類型instanceOopDesc 的定義在 Hotspot 源 碼 中 的instanceOop.hpp 文件中,另外,arrayOopDesc 的定義對應(yīng) arrayOop.hpp

從 instanceOopDesc 代碼中可以看到 instanceOopDesc繼承自 oopDesc,oopDesc 的定義載 Hotspot 源碼中的oop.hpp 文件中在普通實例對象中,oopDesc 的定義包含兩個成員,分別是 _mark 和 _metadata_mark 表示對象標(biāo)記、屬于 markOop 類型,也就是接下來要講解的 Mark World,它記錄了對象和鎖有關(guān)的信息_metadata 表示類元信息,類元信息存儲的是對象指向它的類元數(shù)據(jù)(Klass)的首地址,其中 Klass 表示普通指針、compressedklass 表示壓縮類
MarkWord
在 Hotspot 中,markOop 的定義在 markOop.hpp 文件中,代碼如下

Mark word 記錄了對象和鎖有關(guān)的信息,當(dāng)某個對象被synchronized 關(guān)鍵字當(dāng)成同步鎖時,那么圍繞這個鎖的一系列操作都和 Mark word 有關(guān)系。Mark Word 在 32 位虛擬機(jī)的長度是 32bit、在 64 位虛擬機(jī)的長度是 64bit。Mark Word 里面存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化,Mark Word 可能變化為存儲 5種情況分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖、GC標(biāo)記

為什么任何對象都可以實現(xiàn)鎖
- 首先,Java 中的每個對象都派生自 Object 類,而每個Java Object 在 JVM 內(nèi)部都有一個 native 的 C++對象oop/oopDesc 進(jìn)行對應(yīng)。
- 線程在獲取鎖的時候,實際上就是獲得一個監(jiān)視器對象(monitor) ,monitor 可以認(rèn)為是一個同步對象,所有的Java 對象是天生攜帶 monitor。在 hotspot 源碼的markOop.hpp 文件中,可以看到下面這段代碼

多個線程訪問同步代碼塊時,相當(dāng)于去爭搶對象監(jiān)視器修改對象中的鎖標(biāo)識,上面的代碼中ObjectMonitor這個對象和線程爭搶鎖的邏輯有密切的關(guān)系
synchronized 鎖的升級
在分析 markword 時,提到了偏向鎖、輕量級鎖、重量級鎖。在分析這幾種鎖的區(qū)別時,我們先來思考一個問題使用鎖能夠?qū)崿F(xiàn)數(shù)據(jù)的安全性,但是會帶來性能的下降。不使用鎖能夠基于線程并行提升程序性能,但是卻不能保證線程安全性。這兩者之間似乎是沒有辦法達(dá)到既能滿足性能也能滿足安全性的要求。hotspot 虛擬機(jī)的作者經(jīng)過調(diào)查發(fā)現(xiàn),大部分情況下,加鎖的代碼不僅僅不存在多線程競爭,而且總是由同一個線程多次獲得。所以基于這樣一個概率,synchronized 在JDK1.6 之后做了一些優(yōu)化,為了減少獲得鎖和釋放鎖帶來的性能開銷,引入了偏向鎖、輕量級鎖的概念。因此大家會發(fā)現(xiàn)在 synchronized 中,鎖存在四種狀態(tài)分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖; 鎖的狀態(tài)根據(jù)競爭激烈的程度從低到高不斷升級。
偏向鎖的基本原理
前面說過,大部分情況下,鎖不僅僅不存在多線程競爭,而是總是由同一個線程多次獲得,為了讓線程獲取鎖的代價更低就引入了偏向鎖的概念。怎么理解偏向鎖呢?當(dāng)一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當(dāng)前線程的 ID,后續(xù)這個線程進(jìn)入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖,而是直接比較對象頭里面是否存儲了指向當(dāng)前線程的偏向鎖。如果相等表示偏向鎖是偏向于當(dāng)前線程的,就不需要再嘗試獲得鎖了
偏向鎖的獲取和撤銷邏輯
首先獲取鎖 對象的 Markword,判斷是否處于可偏向狀態(tài)。(biased_lock=1、且 ThreadId 為空)
-
如果是可偏向狀態(tài),則通過 CAS 操作,把當(dāng)前線程的 ID寫入到 MarkWord
- 如果 cas 成功,那么 markword 就會變成這樣。表示已經(jīng)獲得了鎖對象的偏向鎖,接著執(zhí)行同步代碼塊
- 如果 cas 失敗,說明有其他線程已經(jīng)獲得了偏向鎖,這種情況說明當(dāng)前鎖存在競爭,需要撤銷已獲得偏向鎖的線程,并且把它持有的鎖升級為輕量級鎖(這個操作需要等到全局安全點(diǎn),也就是沒有線程在執(zhí)行字節(jié)碼)才能執(zhí)行
-
如果是已偏向狀態(tài),需要檢查 markword 中存儲的ThreadID 是否等于當(dāng)前線程的 ThreadID
- 如果相等,不需要再次獲得鎖,可直接執(zhí)行同步代碼塊
- 如果不相等,說明當(dāng)前鎖偏向于其他線程,需要撤銷偏向鎖并升級到輕量級鎖
偏向鎖的撤銷
偏向鎖的撤銷并不是把對象恢復(fù)到無鎖可偏向狀態(tài)(因為偏向鎖并不存在鎖釋放的概念),而是在獲取偏向鎖的過程中,發(fā)現(xiàn) cas 失敗也就是存在線程競爭時,直接把被偏向的鎖對象升級到被加了輕量級鎖的狀態(tài)。對原持有偏向鎖的線程進(jìn)行撤銷時,原獲得偏向鎖的線程有兩種情況:
- 原獲得偏向鎖的線程如果已經(jīng)退出了臨界區(qū),也就是同步代碼塊執(zhí)行完了,那么這個時候會把對象頭設(shè)置成無鎖狀態(tài)并且爭搶鎖的線程可以基于 CAS 重新偏向當(dāng)前線程
- 如果原獲得偏向鎖的線程的同步代碼塊還沒執(zhí)行完,處于臨界區(qū)之內(nèi),這個時候會把原獲得偏向鎖的線程升級為輕量級鎖后繼續(xù)執(zhí)行同步代碼塊
在我們的應(yīng)用開發(fā)中,絕大部分情況下一定會存在 2 個以上的線程競爭,那么如果開啟偏向鎖,反而會提升獲取鎖的資源消耗。所以可以通過 jvm 參數(shù)
UseBiasedLocking 來設(shè)置開啟或關(guān)閉偏向鎖
流程圖分析

輕量級鎖的基本原理
輕量級鎖的加鎖和解鎖邏輯
鎖升級為輕量級鎖之后,對象的 Markword 也會進(jìn)行相應(yīng)的的變化。升級為輕量級鎖的過程:
- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord
- 將鎖對象的對象頭中的MarkWord復(fù)制到線程的剛剛創(chuàng)建的鎖記錄中
- 將鎖記錄中的 Owner 指針指向鎖對象
- 將鎖對象的對象頭的 MarkWord替換為指向鎖記錄的指針


自旋鎖
輕量級鎖在加鎖過程中,用到了自旋鎖。所謂自旋,就是指當(dāng)有另外一個線程來競爭鎖時,這個線程會在原地循環(huán)等待,而不是把該線程給阻塞,直到那個獲得鎖的線程釋放鎖之后,這個線程就可以馬上獲得鎖的。注意,鎖在原地循環(huán)的時候,是會消耗 cpu 的,就相當(dāng)于在執(zhí)行一個啥也沒有的 for 循環(huán)。所以,輕量級鎖適用于那些同步代碼塊執(zhí)行的很快的場景,這樣,線程原地等待很短的時間就能夠獲得鎖了。自旋鎖的使用,其實也是有一定的概率背景,在大部分同步代碼塊執(zhí)行的時間都是很短的。所以通過看似無異議的循環(huán)反而能提升鎖的性能。但是自旋必須要有一定的條件控制,否則如果一個線程執(zhí)行同步代碼塊的時間很長,那么這線程不斷的循環(huán)反而會消耗 CPU 資源。默認(rèn)情況下自旋的次數(shù)是 10 次,可以通過 preBlockSpin 來修改。在 JDK1.6 之后,引入了自適應(yīng)自旋鎖,自適應(yīng)意味著自旋的次數(shù)不是固定不變的,而是根據(jù)前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態(tài)來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對更長的時間。如果對于某個鎖,自旋很少成功獲得過,那在以后嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費(fèi)處理器資源
輕量級鎖的解鎖
輕量級鎖的鎖釋放邏輯其實就是獲得鎖的逆向邏輯,通過CAS 操作把線程棧幀中的 LockRecord 替換回到鎖對象的MarkWord 中,如果成功表示沒有競爭。如果失敗,表示當(dāng)前鎖存在競爭,那么輕量級鎖就會膨脹成為重量級鎖。
流程圖分析

重量級鎖的基本原理
當(dāng)輕量級鎖膨脹到重量級鎖之后,意味著線程只能被掛起阻塞來等待被喚醒了。
重量級鎖的 monitor
每一個 JAVA 對象都會與一個監(jiān)視器 monitor 關(guān)聯(lián),我們可以把它理解成為一把鎖,當(dāng)一個線程想要執(zhí)行一段被synchronized 修飾的同步方法或者代碼塊時,該線程得先獲取到 synchronized 修飾的對象對應(yīng)的 monitor。monitorenter 表示去獲得一個對象監(jiān)視器。monitorexit 表示釋放 monitor 監(jiān)視器的所有權(quán),使得其他被阻塞的線程可以嘗試去獲得這個監(jiān)視器monitor 依賴操作系統(tǒng)的 MutexLock(互斥鎖)來實現(xiàn)的, 線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個會導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能。
重量級鎖的加鎖的基本流程

任意線程對 Object(Object 由 synchronized 保護(hù))的訪問,首先要獲得 Object 的監(jiān)視器。如果獲取失敗,線程進(jìn)入同步隊列,線程狀態(tài)變?yōu)?BLOCKED。當(dāng)訪問 Object 的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監(jiān)視器的獲取。
回顧線程的競爭機(jī)制
再來回顧一下線程的競爭機(jī)制對于鎖升級這塊的一些基本流程。方便大家更好的理解加入有這樣一個同步代碼塊,存在 Thread#1、Thread#2 等多個線程
synchronized (lock) {
// do something
}
情況一:只有 Thread#1 會進(jìn)入臨界區(qū);
情況二:Thread#1 和 Thread#2 交替進(jìn)入臨界區(qū),競爭不激烈;
情況三:Thread#1/Thread#2/Thread3… 同時進(jìn)入臨界區(qū),競爭激烈
偏向鎖
此時當(dāng) Thread#1 進(jìn)入臨界區(qū)時,JVM 會將 lockObject 的對象頭 Mark Word 的鎖標(biāo)志位設(shè)為“01”,同時會用 CAS 操作把 Thread#1 的線程 ID 記錄到 Mark Word 中,此時進(jìn)入偏向模式。所謂“偏向”,指的是這個鎖會偏向于 Thread#1,若接下來沒有其他線程進(jìn)入臨界區(qū),則 Thread#1 再出入臨界區(qū)無需再執(zhí)行任何同步操作。也就是說,若只有Thread#1 會進(jìn)入臨界區(qū),實際上只有 Thread#1 初次進(jìn)入臨界區(qū)時需要執(zhí)行 CAS 操作,以后再出入臨界區(qū)都不會有同步操作帶來的開銷。
輕量級鎖
偏向鎖的場景太過于理想化,更多的時候是 Thread#2 也會嘗試進(jìn)入臨界區(qū), 如果 Thread#2 也進(jìn)入臨界區(qū)但是Thread#1 還沒有執(zhí)行完同步代碼塊時,會暫停 Thread#1并且升級到輕量級鎖。Thread#2 通過自旋再次嘗試以輕量級鎖的方式來獲取鎖。
重量級鎖
如果 Thread#1 和 Thread#2 正常交替執(zhí)行,那么輕量級鎖基本能夠滿足鎖的需求。但是如果 Thread#1 和 Thread#2同時進(jìn)入臨界區(qū),那么輕量級鎖就會膨脹為重量級鎖,意味著 Thread#1 線程獲得了重量級鎖的情況下,Thread#2就會被阻塞