站在逼了!請站在JVM角度談談Java的鎖?

  • 存在的問題
  • 自旋說
  • 自適應自旋
  • Java 對象的內存佈局(重要)
  • synchronized 鎖升級流程
  • 偏向鎖
  • 輕量級鎖
  • 重量級鎖
  • 可重入
  • 悲觀鎖(互斥鎖、排他鎖)

併發是從JDK 5升級到JDK 6後一項重要的改進項,HotSpot虛擬機開發團隊在這個版本上花費了大量的資源去實現各種鎖優化技術,如適應性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖膨脹(Lock Coarsening)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)等,這些技術都是爲了在線程之間更高效地共享數據及解決競爭問題,從而提高程序的執行效率 .

存在的問題

對於最開始 (JDK1.5之前), Java的同步只能是一個synchronized修飾, 進行同步, 但是這個有很大的問題. 只會有一個線程可以entermonitor , 然後計數器+1. 稱爲重量級鎖。

其他線程都被掛起, 我們知道對於大多數JVM來說, 線程是和操作系統的線程是一一綁定的, 也就是我操作的線程掛起需要由內核來完成, 這時候就需要用戶態轉換到內核態 ,然後內核執行此線程掛起, 當要恢復線程的時候再通知內核, 此時會造成很嚴重的問題。

我們知道對於CPU來說, 它是靠時間片來實現的多線程並行執行, 如果我一個同步任務只會比如count++ , 他執行很短, 短到幾ns級別, 而掛起線程和恢復線程的實現遠遠大於幾ns , 可能大幾個量級。

 

因此聰明的人想到一個事情, 就是我不讓你掛起, 這麼短我就自己空轉一會, 也很短, (空轉的意思其實就是while(true) 啥也不做,但是不是讓CPU掛起,這個也稱之爲自旋) , 我們知道空轉就是一種浪費CPU的事情 , 但是這個浪費得有個度 , 我們上訴的問題, 每個線程可能空轉的時間也就幾ns , 但是對於長到幾秒的還能空轉嗎, 不行了. 所以這裏就是一個劃分點。

 

還有一個問題就是, 比如某一段時間內, 就一個線程處於運作中, 那麼此時還需要加鎖操作嗎 ? 是否需要優化。

因此引出了下文的解決方案。

自旋鎖

自旋鎖是JDK1.4.2的時候引入的, 默認爲關閉狀態, 可以使用-XX:+UseSpinning參數來開啓 , 但是這個自旋鎖他不是一直的自旋, 他有個度, 這個度可以用-XX:PreBlockSpin 來控制自旋多少次, 默認是10次。

自適應自旋

JDK 6中對自旋鎖的優化,引入了自適應的自旋。

自適應意味着自旋的時間不再是固定的了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而允許自旋等待持續相對更長的時間,比如持續100次忙循環。

 

另一方面,如果對於某個鎖,自旋很少成功獲得過鎖,那在以後要獲取這個鎖時將有可能直接省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨着程序運行時間的增長及性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越精準,虛擬機就會變得越來越“聰明”了。

Java 對象的內存佈局(重要)

瞭解輕量級鎖和偏向鎖 需要了解Java對象的內存佈局.

再看下面之前 , 要了瞭解一個JAVA對象的內存結構 , 也稱之爲對象的內存佈局

懵逼了!請站在 JVM 角度談談 Java 的鎖?

 

對象頭 :

1、對象自身的運行時數據( MarkWord )

存儲 hashCode、GC 分代年齡、鎖類型標記、偏向鎖線程 ID 、CAS 鎖指向線程 LockRecord 的指針等,synconized 鎖的機制與這個部分( markwork )密切相關,用 markword 中最低的三位代表鎖的狀態,其中一位是偏向鎖位,另外兩位是普通鎖位。

 

關於markword , 這個是32位操作系統的實現,

懵逼了!請站在 JVM 角度談談 Java 的鎖?

 

2、對象類型指針( Class Pointer )

對象指向它的類元數據的指針(這個指針類似於C語言的指針, 指針大小是根據操作系統決定的,64位好像是8個字節大小, 因爲64位系統的尋址空間很大), JVM 就是通過它來確定是哪個 Class 的實例。

如果是數組對象,還會有一個額外的部分用於存儲數組長度。因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是如果數組的長度是不確定的,將無法通過元數據中的信息推斷出數組的大小。也就是arr.len調用很方便.

實例數據區域

此處存儲的是對象真正有效的信息,比如對象中所有字段的內容 . ,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。

這部分的存儲順序會受到虛擬機分配策略參數(-XX:FieldsAllocationStyle參數)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配順序爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)(這裏基本可以確定Java的類型也就8種),從以上默認的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果HotSpot虛擬機的+XX:CompactFields參數值爲true(默認就爲true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節省出一點點空間。

對齊填充

對象的第三部分是對齊填充,這並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),因此,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。

其實也是爲了存儲方便.

如果你還是對上述不理解的話, 你就看看 <深入理解Java虛擬機> , 裏面有. 接下來就看看具體內容了 .

synchronized 鎖升級流程

synchronized 鎖並不是直接進去就是一個重量級鎖, 而是有所思考的, 因爲很多短的操作,並不需要掛起線程. 所以類似於空轉 , 還有就是單線程加鎖. 何必掛起線程呢, 所以sync也幫助我們解決了這個問題.

懵逼了!請站在 JVM 角度談談 Java 的鎖?

 

偏向鎖

在 JDK1.8 中,其實默認是輕量級鎖,但如果設定了-XX:BiasedLockingStartupDelay=0 ,那在對一個 Object 做 syncronized 的時候,會立即上一把偏向鎖。當處於偏向鎖狀態時, markwork 會記錄當前線程 ID。

它的意思是這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖一直沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。偏向鎖解決的問題是, 有些時候就一個線程在運行, 難道還有多線程問題嗎, 所以並不需要. 當出現第二個線程去競爭的情況下才會出現降級 .

原理: 當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設置爲“01”、把偏向模式設置爲“1”,表示進入偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中。如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如加鎖、解鎖及對Mark Word的更新操作等)。一旦出現另外一個線程去嘗試獲取這個鎖的情況,偏向模式就馬上宣告結束。

輕量級鎖

當下一個線程參與到偏向鎖競爭時,會先判斷 markword 中保存的線程 ID 是否與這個線程 ID 相等,如果不相等,會立即撤銷偏向鎖,升級爲輕量級鎖。每個線程在自己的線程棧中生成一個 LockRecord ( LR ),然後每個線程通過 CAS (自旋 )的操作將鎖對象頭中的 markwork 設置爲指向自己的 LR 的指針,哪個線程設置成功,就意味着獲得鎖。 關於 synchronized 中此時執行的 CAS 操作是通過 native 的調用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代碼實現的,有興趣的可以繼續深挖。

重量級鎖

如果說競爭加劇(如線程自旋次數或者自旋的線程數超過某閾值, JDK1.6 之後,由 JVM 自己控制該規則),就會升級爲重量級鎖。此時就會向操作系統申請資源,線程掛起,進入到操作系統內核態的等待隊列中,等待操作系統調度,然後映射回用戶態。在重量級鎖中,由於需要做內核態到用戶態的轉換,而這個過程中需要消耗較多時間,也就是"重"的原因之一。

可重入

synchronized 擁有強制原子性的內部鎖機制,是一把可重入鎖。因此,在一個線程使用 synchronized 方法是調用該對象另一個 synchronized 方法,即一個線程得到一個對象鎖後再次請求該對象鎖,是永遠可以拿到鎖的。在 Java 中線程獲得對象鎖的操作是以線程爲單位的,而不是以調用爲單位的。

synchronized 鎖的對象頭的 markwork 中會記錄該鎖的線程持有者和計數器,當一個線程請求成功後, JVM 會記下持有鎖的線程,並將計數器計爲1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。當線程退出一個 synchronized 方法/塊時,計數器會遞減,如果計數器爲 0 則釋放該鎖鎖。

悲觀鎖(互斥鎖、排他鎖)

synchronized 是一把悲觀鎖(獨佔鎖),當前線程如果獲取到鎖,會導致其它所有需要鎖該的線程等待,一直等待持有鎖的線程釋放鎖才繼續進行鎖的爭搶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章