今天筆者在閱讀《Java并發編程的藝術》時,閱讀到了Java并發機制的底層實作原理一章中的synchronized部分,覺得作者對Java物件頭的描述有些模糊,然后在筆者腦海中,就想起來了前一陣閱讀《深入理解Java虛擬機》時,正好閱讀過Java物件的記憶體布局,不過由于筆者智商較低,對這一部分的記憶也有些許模糊了…
正所謂:好記性不如爛筆頭,于是!筆者將這兩個“模糊”的部分結合到一個文章中,以加深自己的記憶,
這篇文章完全是站在巨人的肩膀上,如果想深入了解Java并發編程與JVM可以去閱讀下面的參考書籍
參考書籍:《Java并發編程的藝術》與《深入理解Java虛擬機·JVM高級特性與最佳實踐》
吐槽:Typora為什么沒有合并單元格的功能,HTML手敲表格真的好累!!!!!
synchronized的實作原理
這里直接參考書中的內容
JVM基于進入和退出Monitor物件來實作方法同步和代碼塊同步,但兩者的實作細節不一樣,代碼塊同步是使用monitorenter和monitorexit指令實作的,而方法同步是使用另外一種方式實作的,細節在JVM規范里并沒有詳細說明,但是,方法的同步同樣可以使用這兩個指令來實作,
monitorenter是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和例外處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對,任何物件都有一個monitor與之關聯,當且一個monitor被持有后,它將處于鎖定狀態,執行緒執行到monitorexit指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖,
synchronized的應用
Java中的每一個物件都可以作為鎖,也就是我們常說的“Java中每個物件都持有一把鎖”,這是synchronized實作同步的基礎,
synchronized實作同步具體表現為以下三種形式:
- 對于普通同步方法,鎖是當前實體物件
- 對于靜態同步方法,鎖是當前類的Class物件
- 對于同步方法塊,鎖是synchronized括號里配置的物件
學習過多執行緒的讀者都知道,當一個執行緒試圖訪問同步代碼塊時,它必須首先得到物件的鎖,退出或者拋出例外時必須釋放鎖,
那么鎖到底在哪里呢?鎖里面會存盤什么資訊呢?
Java物件的記憶體布局
在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體資料(Instance Data)和對其填充(Padding),下面分別講述這三部分,
Java物件頭
HotSpot虛擬機物件的物件頭包括兩類資訊,但如果物件是一個Java陣列,那么物件頭中還必須有一塊用于記錄陣列長度的資料,這部分就不作重點說明了,我們主要看前兩類資訊,
第一類是用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機中分別為32個位元和64個位元,官方稱它為“Mark Word”,
以32位HotSpot虛擬機為例,當物件未被同步鎖鎖定時,Mark Word的32個位元存盤空間中的25個位元用于存盤物件哈希碼,4個位元用于存盤物件分代年齡,2個位元用于存盤鎖標志位,1個位元固定為0,如下表所示:
| 鎖狀態 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖標志位 |
|---|---|---|---|---|
| 無鎖狀態 | 物件的HashCode | 物件分代年齡 | 0 | 01 |
在運行期間,Mark Word里存盤的資料會隨著鎖標志位的變化而變化,Mark Word可能變化為存盤以下4中資料,如表所示:
| 鎖狀態 | 25bit | 4bit | 1bit | 2bit | |
|---|---|---|---|---|---|
| 23bit | 2bit | 是否是偏向鎖 | 鎖標志位 | ||
| 輕量級鎖 | 指向堆疊中鎖記錄的指標 | 00 | |||
| 重量級鎖 | 指向互斥量(重量級鎖)的指標 | 10 | |||
| GC標記 | 空 | 11 | |||
| 偏向鎖 | 執行緒ID | Epoch | 物件分代年齡 | 1 | 01 |
在64位虛擬機下,Mark Word的存盤結構如表所示:
| 鎖狀態 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
|---|---|---|---|---|---|---|
| cms_free | 分代年齡 | 偏向鎖 | 鎖標志位 | |||
| 無鎖 | unused | HashCode | 0 | 01 | ||
| 偏向鎖 | ThreadID(54bit) Epoch(2bit) | 1 | 01 | |||
物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,Java虛擬機通過這個指標來確定是哪個類的實體,
實體資料部分
實體資料部分是物件真正存盤的有效資訊,即我們在程式代碼里所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來,這部分的存盤順序會受到虛擬機分配策略引數和欄位在Java原始碼中定義順序的影響,HotSpot虛擬機默認的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPS),從以上默認的分配策略中可以看到,相同寬度的欄位總是被被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前,
對齊填充
物件的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起者占位符的作用,由于Hotspot虛擬機的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是任何物件的大小都必須是8位元組的整數倍,物件頭部分已經被精心設計成正好是8位元組的倍數,因此,如果物件實體資料部分沒有對齊的話,就需要通過對齊填充來補全,
鎖的升級與優化
在多執行緒并發編程中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級,鎖可以升級但卻不可以降級,目的是為了提高獲得鎖和釋放鎖的效率,
偏向鎖
HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引人了偏向鎖,
下面就分析一下執行緒獲得偏向鎖所經歷的程序:
當一個執行緒訪問同步塊并獲取鎖時,會在物件頭和堆疊幀中的鎖記錄里存盤鎖偏向的執行緒ID,以后該執行緒在進人和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測驗一下物件頭的Mark Word里是否存盤著指向當前執行緒的偏向鎖,
如果測驗成功,表示執行緒已經獲得了鎖,
如果測驗失敗則需要再測驗一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):
- 如果沒設定,則使用CAS競爭鎖;
- 如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒,
對于CAS這里要簡單說明一下:Java的Atomic包使用CAS演算法來更新資料,而不需要加鎖,也就是一種更新資料的方法,
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,
它首先會暫停擁有偏向鎖的執行緒,然后檢查持有偏向鎖的執行緒是否活著
- 如果執行緒不處于活動狀態,則將物件頭設定成無鎖狀態
- 如果執行緒仍然活著,擁有偏向鎖的堆疊會被執行,遍歷偏向物件的鎖記錄,堆疊中的鎖記錄和物件頭的Mark Word要么重新偏向于其他執行緒,要么恢復到無鎖或者標記物件不適合作為偏向鎖
最后喚醒暫停的執行緒
輕量級鎖
輕量級鎖加鎖
執行緒在執行同步塊之前,JVM會先在當前執行緒的堆疊楨中創建用于存盤鎖記錄的空間,并將物件頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word,然后執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標,如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖,
輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如果成功,則表示沒有競爭發生,如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖,
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態,當鎖處于這個狀態下,其他執行緒試圖獲取鎖時,都會被阻塞住,當持有鎖的執行緒釋放鎖之后會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭,
最后希望大家保持自律,努力學習!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/385461.html
標籤:其他
上一篇:ubuntu18.04配置deepo深度學習環境(cuda + cudnn + nvidia-docker + deepo)--超級細致,并把遇到的錯誤和所有解決方案都列出來了
