一、加鎖發生了什么
//System.out.println都加了鎖
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
簡單加鎖發生了什么?
要弄清楚加鎖之后到底發生了什么需要看一下物件創建之后再記憶體中的布局是個什么樣的?
一個物件在 new 出來之后在記憶體中主要分為 4 個部分:
- Markword 這部分其實就是加鎖的核心,同時還包含的物件的一些生命資訊,例如是否 GC、進過了幾次 Young GC 還存活等,
- klass pointer 記錄了指向物件的 class 檔案指標,
- instance data 記錄了物件里面的變數資料,
- padding 作為對齊使用,物件在 64 位服務器版本中,規定物件記憶體必須要能被 8 位元組整除,如果不能整除,那么就靠對齊來補,舉個例子:new 出了一個物件,記憶體只占用 18 位元組,但是規定要能被 8 整除,所以 padding=6,

知道了這 4 個部分之后,我們來驗證一下底層,借助于第三方包 JOL = Java Object Layout java 記憶體布局去看看,很簡單的幾行代碼就可以看到記憶體布局的樣式:
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
將結果列印出來:
從輸出結果看:
-
物件頭包含了 12 個位元組分為 3 行,其中前 2 行其實就是 Markword,第三行就是 klass 指標,值得注意的是在加鎖前后輸出從 001 變成了 000,Markword 用處:8 位元組(64bit)的頭記錄一些資訊,鎖就是修改了 Markword 的內容 8 位元組(64bit)的頭記錄一些資訊,鎖就是修改了markword的內容位元組(64bit)的頭記錄一些資訊,從 001 無鎖狀態,變成了 00 輕量級鎖狀態,

-
new 出一個 object 物件,占用 16 個位元組,物件頭占用 12 位元組,由于 Object 中沒有額外的變數,所以 instance = 0,考慮要物件記憶體大小要被 8 位元組整除,那么 padding=4,最 后 new Object() 記憶體大小為 16 位元組,
二、鎖的升級程序
2.1 鎖的升級驗證
探討鎖的升級之前,先做個實驗,兩份代碼,不同之處在于一個中途讓它睡了5秒,一個沒睡,看看是否有區別,
public class JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
----------------------------------------------------------------------------------------------
public class JOLDemo {
private static Object o;
public static void main(String[] args) {
try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
這兩份代碼會不會有什么區別?運行之后看看結果:
有點意思的是,讓主執行緒睡了 5s 之后輸出的記憶體布局跟沒睡的輸出結果居然不一樣,Syn 鎖升級之后,jdk1.8 版本的一個底層默認設定 4s 之后偏向鎖開啟,也就是說在 4s 內是沒有開啟偏向鎖的,加了鎖就直接升級為輕量級鎖了,
那么這里就有幾個問題了?
- 為什么要進行鎖升級,以前不是默認 syn 就是重量級鎖么?要么不用要么就用別的不行么?
- 既然 4s 內如果加了鎖就直接到輕量級,那么能不能不要偏向鎖,為什么要有偏向鎖?
- 為什么要設定 4s 之后開始偏向鎖?
問題 1:為什么要進行鎖升級?鎖了就鎖了,不就要加鎖么?
首先明確 syn 鎖 在 jdk1.2 之前效率非常低,那時候 syn 就是重量級鎖,申請鎖必須要經過作業系統老大 kernel 進行系統呼叫,入隊進行排序操作,操作完之后再回傳給用戶態,
內核態:用戶態如果要做一些比較危險的操作直接訪問硬體,很容易把硬體搞死(格式化,訪問網卡,訪問記憶體干掉等),作業系統為了系統安全分成兩層:用戶態和內核態,申請鎖資源的時候用戶態要向作業系統老大內核態申請,Jdk1.2 的時候用戶需要跟內核態申請鎖,然后內核態還會給用戶態,這個程序是非常消耗時間的,導致早期效率特別低,有些 jvm 就可以處理的為什么還交給作業系統做去呢?能不能把 jvm 就可以完成的鎖操作拉取出來提升效率,所以也就有了鎖優化,
問題 2:為什么要有偏向鎖?
其實這本質上歸根于一個概率問題,統計表示,在我們日常用的 syn 鎖程序中 70%-80% 的情況下,一般都只有一個執行緒去拿鎖,例如我們常使用的 System.out.println、StringBuffer,雖然底層加了 syn 鎖,但是基本沒有多執行緒競爭的情況,那么這種情況下,沒有必要升級到輕量級鎖級別了,
偏向的意義在于:第一個執行緒拿到鎖,將自己的執行緒資訊標記在鎖上,下次進來就不需要在拿去拿鎖驗證了,如果超過 1 個執行緒去搶鎖,那么偏向鎖就會撤銷,升級為輕量級鎖,其實我認為嚴格意義上來講偏向鎖并不算一把真正的鎖,因為只有一個執行緒去訪問共享資源的時候才會有偏向鎖這個情況,
問題 3:為什么 jdk8 要在 4s 后開啟偏向鎖?
其實這是一個妥協,明確知道在剛開始執行代碼時,一定有好多執行緒來搶鎖,如果開了偏向鎖效率反而降低,所以上面程式在睡了 5s 之后偏向鎖才開放,為什么加偏向鎖效率會降低,因為中途多了幾個額外的程序,上了偏向鎖之后多個執行緒爭搶共享資源的時候要進行鎖升級到輕量級鎖,這個程序還的把偏向鎖進行撤銷在進行升級,所以導致效率會降低,為什么是 4s?這是一個統計的時間值,
當然我們是可以禁止偏向鎖的,通過配置引數 -XX:-UseBiasedLocking = false 來禁用偏向鎖,jdk15 之后默認已經禁用了偏向鎖,本文是在 jdk8 的環境下做的鎖升級驗證,
2.2 鎖的升級流程
上面已經驗證了物件從創建出來之后進記憶體從無鎖狀態->偏向鎖(如果開啟了)->輕量級鎖的程序,對于鎖升級的流程繼續往下,輕量級鎖之后就會變成重量級鎖,首先我們先理解什么叫做輕量級鎖,從一個執行緒搶占資源(偏向鎖)到多執行緒搶占資源升級為輕量級鎖,執行緒如果沒那么多的話,其實這里就可以理解為 CAS(Compare and Swap:比較并交換值),
問題 4:什么情況下輕量級鎖要升級為重量級鎖呢?
首先我們可以思考的是多個執行緒的時候先開啟輕量級鎖,如果它 carry 不了的情況下才會升級為重量級,那么什么情況下輕量級鎖會 carry 不住?
- 如果執行緒數太多,比如上來就是 10000 個,那么這里 CAS 要轉多久才可能交換值,同時 CPU 光在這 10000 個活著的執行緒中來回切換中就耗費了巨大的資源,這種情況下自然就升級為重量級鎖,直接叫給作業系統入隊管理,那么就算 10000 個執行緒那也是處理休眠的情況等待排隊喚醒,
- CAS 如果自旋 10 次依然沒有獲取到鎖,那么也會升級為重量級,
總的來說,兩種情況都會從輕量級升級為重量級,10 次自旋或等待 cpu 調度的執行緒數超過 cpu 核數的一半,自動升級為重量級鎖,整個鎖升級程序如圖所示:

問題 5:都說 syn 為重量級鎖,那么到底重在哪里?
JVM 偷懶把任何跟執行緒有關的操作全部交給作業系統去做,例如調度鎖的同步直接交給作業系統去執行,而在作業系統中要執行先要入隊,另外作業系統啟動一個執行緒時需要消耗很多資源,消耗資源比較重,重就重在這里,
原文鏈接:
談談JVM內部鎖升級程序
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288745.html
標籤:Java
上一篇:1.前言-聊聊Java這條路
下一篇:2.預科-走進計算機
