synchronized相信你們都比較熟悉,面試的時候也是必備問題。有不少書籍介紹它,網上也有不少不少相關的文字,我本身也看過不少。可是看了就忘,印象不深,因而我決定閱讀源碼來加深記憶。代碼出真知,這一看沒關係,不只讓我對synchronized有了更深刻的理解,我還發現,網上許多文章,甚至不少面試官的理解都是錯誤的。java
好比,說說你對自旋鎖的理解。你能夠先把答案本身在內心想一下,看到下面,你可能會發現,原來你原來的理解是錯誤的。c++
使用上我就很少說了,相信你們都知道,這裏歸納一下面試
synchronized(obj) { .... some code ... }
這種使用時對obj對象上鎖。也是比較常見的使用。segmentfault
synchronized void test() { .... some code ... }
這種事對this進行上鎖多線程
synchronized static void test() { .... some code ... }
這種事對Class對象上鎖。jvm
加入咱們有一個簡單的方法以下oop
public void test() { Object obj = new Object(); synchronized (obj) { System.out.println("in locking"); } }
會編譯成如下字節碼優化
public test()V ... MONITORENTER L0 LINENUMBER 15 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "in locking" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L6 LINENUMBER 16 L6 ALOAD 2 MONITOREXIT L1 GOTO L7 L2 FRAME FULL [com/pinduoduo/service/jmm/JmmTest java/lang/Object java/lang/Object] [java/lang/Throwable] ASTORE 3 ALOAD 2 MONITOREXIT ... }
其餘咱們不要管,能夠看到字節碼中有一個MONITOREXIT
指令和兩個MONITOREXIT
指令。兩個MONITOREXIT
是由於要異常的狀況也也要進行MONITOREXIT
.this
咱們在上小節知道了synchronized塊是有MONITORENTER
和MONITOREXIT
指令包裹着的,那這兩個指令怎麼執行呢?這就涉及的jvm怎麼實現的了,這裏咱們就以hotspot爲例,看看他是怎麼實現的。spa
hotspot的這兩個指令執行入口在bytecodeInterpreter.cpp
中的CASE(_monitorenter)
和CASE(_monitorexit)
中,接着就是一堆c++代碼了,這裏咱們不深刻,否則你們確定睡着了,有興趣的小夥伴能夠本身去看看,我其餘文章也會具體分析。
這一小節會簡單的說一說synchronized的原理,都是根據我閱讀源碼的總結。
偏向鎖適用於沒有多個線程會進入的資源。這個點在說輕量級鎖的時候會再次提到。
偏向鎖又稱無鎖,由於它只改變鎖對象頭的markWord。
在Hotspot中,每一個java對象並非對應一個c++對象,而是用oop-klass這種二分模型表示,關於這個二分模型咱們也不展開說。oop中一個一個markOop,他的類型是 markOopDesc*,是一個指針。而markword就是指針的值,而不是指針指向的地址的值。啥意思呢?好比64位機器上,markOop就是一個64位的數值而已,這一點也是我饒了好久才懂。這裏只是提一下,不重要。
結構圖以下:
偏向鎖狀態下,markword的鎖標誌位會變成01,是否偏向標誌爲1,此外epoch只一個標誌,用因而否須要批量重定向的判斷,咱們先無論它。那如何證實偏向鎖狀態下,鎖的markword會變成這個樣子呢?咱們的jol就登場了,跟我一塊兒看接下來的小demo,耐心。
首先咱們引入jol的依賴
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
代碼以下
import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; public class MarkWordTest { public static void main(String[] args) throws InterruptedException { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } MarkWordTest object = new MarkWordTest(); String classLayout1 = ClassLayout.parseInstance(object).toPrintable(); System.out.println("---------------------------加鎖前---------------------------"); System.out.println(classLayout1); System.out.println("------------------------------------------------------------"); synchronized (object) { //打印當前jvm信息 System.out.println("---------------------------加鎖中---------------------------"); String classLayout2 = ClassLayout.parseInstance(object).toPrintable(); System.out.println(classLayout2); System.out.println("------------------------------------------------------------"); } System.out.println("---------------------------加鎖後---------------------------"); String classLayout3 = ClassLayout.parseInstance(object).toPrintable(); System.out.println(classLayout3); System.out.println("------------------------------------------------------------"); } }
首先注意到,代碼開始有一個sleep 5秒的操做,是由於偏向鎖默認有個延遲啓動時間,默認爲4s。
而後分別在加鎖前、中、後對對象頭進行打印,咱們看一下打印結果
---------------------------加鎖前--------------------------- com.pinduoduo.service.jmm.MarkWordTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ------------------------------------------------------------ ---------------------------加鎖中--------------------------- com.pinduoduo.service.jmm.MarkWordTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ------------------------------------------------------------ ---------------------------加鎖後--------------------------- com.pinduoduo.service.jmm.MarkWordTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ------------------------------------------------------------ Process finished with exit code 0
markword是64位,既上面一堆數字的前兩行,咱們刪掉其餘東西,只看markword
---------------------------加鎖前--------------------------- ... 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) ... ------------------------------------------------------------ ---------------------------加鎖中--------------------------- ... 0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) ... ------------------------------------------------------------ ---------------------------加鎖後--------------------------- ... 0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) ... ------------------------------------------------------------
是否是有點看不懂,這是由於,Hopspot是小端儲存,既低位值放在高位地址上。那咱們按照咱們好讀的宗旨把它改成大端存
---------------------------加鎖前--------------------------- 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 ------------------------------------------------------------ ---------------------------加鎖中--------------------------- 00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101 ------------------------------------------------------------ ---------------------------加鎖後--------------------------- 00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101 ------------------------------------------------------------
如今就是咱們能夠讀懂的結構了。
等等,加鎖前爲啥鎖標誌位爲01,偏向鎖模式呢?是的,是這樣的,對象頭初始就是偏向鎖模式,並非不少文章都說的無鎖模式,只是如今尚未指向任何線程。
加鎖中咱們就好理解了,鎖標誌位是偏向鎖,而且線程id也不是0了。
加鎖後能夠看到,markword並無任何改變。這樣作天然有用,就是,當有其餘線程來想進入臨界區的時候,看到偏向線程id不是本身,直接進行鎖升級(不考慮批量重偏向)。
至此咱們能夠得出一個結論:偏向鎖只在只有一個線程會進入臨界區的場景有用。
不知道你有沒有注意到一個問題,就是,既然退出臨界區後,markword沒有任何變化,那鎖重入的計數怎麼實現的?這就涉及到synchronized
的重入了,不論是偏向鎖、輕量鎖仍是重量級鎖,都是使用的同一套重入機制。
在Hopspot中,每一個線程都有一個鎖記錄表,鎖記錄是BasicObjectLock
類型,裏邊有一個obj和displaced_header,obj存儲鎖對象,set_displaced_header鎖對象的markword,每一次進入臨界區,都會建立一個BasicObjectLock
對象,若是是重複,那displaced_header就置爲null.
輕量級鎖適用於線程交替進入臨界區的場景,只要有鎖競爭就會升級成重量級鎖。注意,1.8中<font color=red>沒有自旋鎖</font>,別再說通過自旋後升級成重量級鎖!!!
別嫌我囉嗦,再比較一下偏向鎖與輕量級鎖適用場景的不一樣。
偏向鎖:只有一個線程會進入臨界區
輕量級鎖:不一樣線程交替進入臨界區
偏向鎖和輕量級鎖都是在沒有鎖競爭的條件下適用的。
輕量級鎖的加鎖和釋放都是經過cas操做對鎖對象的markword就行修改而實現的,它的重入與偏向鎖的機制相同,這裏就不贅述了。
重量級鎖的實現是用了監視器模式(moniter),我猜字節碼指令MONITORENTER
、MONITOREXIT
開始指的就是這個操做,後來通過一系列的優化,使這兩個指令的字面意義跟鎖的實現不太對應了。
在Hotspot中,每一個對象(java對象)都會關聯一個ObjectMonitor,這個就是一個監視器。
ObjectMonitor中咱們重點關注如下幾個字段
ObjectMonitor() { _owner = NULL; // 鎖持有者 _recursions = 0; // 重入次數 _WaitSet = NULL; _cxq = NULL ; _EntryList = NULL ; }
_owner
既爲鎖持有者。
_recursions
表示重入次數。從這個字段就能夠看出來,重量級鎖的重入跟輕量級鎖、偏向鎖的重入不一樣,是用計數法來作的。
_cxq
是一個隊列,若是線程獲取鎖失敗,必定是進入這個隊列的。
_EntryList
也是一我的隊列,當ObjectMonitor
持有者釋放鎖後,ObjectMonitor
必定是從這個隊列中喚醒一個線程的。
_WaitSet
是調用wait
操做的線程隊列。
認識這幾個字段後,咱們會很容易理解ObjectMonitor的運做原來,並無網上說的那麼複雜,至於網上常常曬的那張流程圖我就不貼了。下面我歸納一下。
_owner
爲空,則直接用cas去修改_owner
的值,成功則證實獲取鎖成功,不然,會準備進去隊列,在進入隊列前,會通過屢次自旋嘗試cas修改_owner
,這也是爲何synchronized
,是非公平鎖的緣由,就是他會不斷的搶佔式的獲取鎖。_cxq
隊列中。注意,搶佔無果的線程必定是進入_cxq
隊列。_WaitSet
隊列。當獲取鎖的線程調notify或notifyAll時,根據不一樣的策略,_WaitSet
會將節點插入_cxq
或_EntryList
中。
0: 將頭節點插入到EntryList頭部 1: 將頭節點插入到EntryList尾部 2: 將頭節點插入到cxq頭部,默認選項 3: 將頭節點插入到cxq尾部
當鎖空閒的時候,根據不一樣的策略,以不一樣的方式將_cxq
插入到_EntryList
,
QMode = 2且cxq非空:取cxq隊列隊首的ObjectWaiter對象,調用ExitEpilog方法,該方法會喚醒ObjectWaiter對象的線程,而後當即返回,後面的代碼不會執行了; QMode = 3且cxq非空:把cxq隊列插入到EntryList的尾部; QMode = 4且cxq非空:把cxq隊列插入到EntryList的頭部; QMode = 0:暫時什麼都不作
Moniter
實現。根據不一樣策略和操做,線程在 _WaitSet
、 _cxq
、 _EntryList
中流轉,重入經過_recursions計數來實現。