文章目錄
- 1 執行緒安全問題
- 2 初識synchronized
- 2.1 使用場景
- 2.2 案例分析
- 3 synchronized原理
- 3.1 Java物件頭
- 3.2 鎖記錄
- 3.3 Monitor原理
- 4 synchronized進階
- 4.1 重量級鎖
- 4.2 自旋
- 4.2.1 自旋鎖
- 4.2.2 適應性自旋鎖
- 4.3 鎖消除與鎖粗化
- 4.3.1 鎖消除
- 4.3.2 鎖粗化
- 4.4 偏向鎖
- 4.5 輕量級鎖
- 4.6 鎖膨脹/鎖升級
- 5 簡析CAS
- 6 寫在最后
- 參考資料
1 執行緒安全問題
在并發編程中,需要處理兩個關鍵問題:執行緒之間如何通信及執行緒之間如何同步(這里的執行緒是指并發執行的活動物體),
通信是指執行緒之間以何種機制來交換資訊,Java中并發采用的是共享記憶體模型,在共享記憶體的并發模型里,執行緒之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀態進行隱式通信,
而同步是指程式中用于控制不同執行緒間操作發生相對順序的機制,在共享記憶體并發模型里,同步是顯式進行的,程式員必須顯式指定某個方法或某段代碼需要在執行緒之間互斥執行,
Java記憶體模型(Java Memory Model,JMM)描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數存盤到記憶體和從記憶體中讀取出變數這樣的底層細節,

在Java中,所有實體域、靜態域和陣列元素都存盤在堆記憶體中,堆記憶體在執行緒之間共享,由于執行緒的作業記憶體是執行緒私有記憶體,執行緒間無法互相訪問對方的作業記憶體,所以執行緒 0 、執行緒 1 和執行緒 2需要讀寫主記憶體的共享變數時,就都先將該共享變數拷貝(load)到自己的作業記憶體,然后在自己的作業記憶體中對該變數進行所有操作,執行緒作業記憶體對變數副本完成操作之后再將結果同步(save)至主記憶體,
因此,在執行緒背景關系切換期間,多執行緒讀寫共享記憶體中的全域變數及靜態變數容易引發競態條件,
下面用代碼來說明:
@Slf4j
public class SafeTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count的值是: {}", count);
}
}
按照常理而言,count最終結果應該為0,多次運行發現,最終count值還可能是正數,也可能為負數,這是為什么呢?
從位元組碼的層面進行分析:
0 iconst_1
1 istore_0
2 iload_0
3 sipush 5000
6 if_icmpge 23 (+17)
9 getstatic #10 <com/kai/demo/basic/SafeTest.count> // 獲取靜態變數i的值
12 iconst_1 // 準備常量1
13 iadd // 自增
14 putstatic #10 <com/kai/demo/basic/SafeTest.count> // 將修改后的值存入靜態變數i
17 iinc 0 by 1
20 goto 2 (-18)
23 return
0 iconst_1
1 istore_0
2 iload_0
3 sipush 5000
6 if_icmpge 23 (+17)
9 getstatic #10 <com/kai/demo/basic/SafeTest.count> // 獲取靜態變數i的值
12 iconst_1 // 準備常量1
13 isub // 自減
14 putstatic #10 <com/kai/demo/basic/SafeTest.count> // 將修改后的值存入靜態變數i
17 iinc 0 by 1
20 goto 2 (-18)
23 return
可見count++ 和 count-- 操作實際都是需要這個4個指令完成的,那么這里問題就來了!Java 的記憶體模型如下,完成靜態變數的自增、自減需要在主存和作業記憶體中進行資料交換,
正常順序執行:

實際出現負數的情況:

實際出現正數的情況:

像上面count++ 和 count-- 的代碼所在區域又稱臨界區(一段代碼內如果存在對共享資源的多執行緒讀寫操作,那么稱這段代碼為臨界區),
跟上面案例一樣,如果多個執行緒臨界區代碼執行競爭同一資源時,對資源的訪問順序敏感,執行時序的不同導致會出現某種不正常的行為,就稱存在競態條件(Race Condition),
為避免競態條件的出現,保證java共享記憶體的原子性、可見性、有序性,很有必要保持執行緒同步,Java中提供了synchronized、volatile關鍵字與Lock類,
2 初識synchronized
synchronized采用互斥同步(Mutual Exclusion & Synchnronization)的方式,讓多個執行緒并發訪問共享資料時,保證共享資料在同一時刻只被一個(或一些,使用信號量的時候)執行緒使用,互斥是實作同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實作方式,因此在互斥同步四個字中,互斥是因,同步是果;互斥是方法,同步是目的,
2.1 使用場景
在Java代碼中使用synchronized可使用在代碼塊和方法中,根據Synchronized用的位置可以有這些使用場景:

可見,synchronized的使用場景主要有3種:
- 修飾靜態方法,給當前類物件加鎖,進入同步方法時需要獲得類物件的鎖;
- 修飾實體方法,給當前實體變數加鎖,進入同步方法時需要獲得當前實體的鎖;
- 修飾同步方法塊,指定加鎖物件(實體物件/是類變數),進入同步方法塊時需要獲得加鎖物件的鎖,
2.2 案例分析
下面,將synchronized應用到上面的案例中:
@Slf4j
public class SafeTest {
static int count = 0;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
synchronized (lock){
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i < 5000; i++) {
synchronized (lock){
count--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count的值是: {}", count);
}
}
多次測驗發現,結果都為0,這是因為synchronized利用物件鎖保證了臨界區代碼的原子性,臨界區內的代碼在外界看來是不可分割的,不會被執行緒切換所打斷,

synchronized為什么這么神奇,它到底做了什么呢?下面就進一步探討一下synchronized的實作原理,
3 synchronized原理
先來了解兩個重要的概念:“Java物件頭”、“鎖記錄”,
3.1 Java物件頭
物件實體化記憶體布局與訪問定位中描述了,JVM中物件記憶體布局主要分為三塊區域:物件頭區、實體資料區和填充區,
Synchronized用到的鎖就是存在Java物件頭里的,物件頭區又主要分為兩部分,分別是 運行時元資料(Mark Word)和 型別指標,
如果物件是陣列型別,則虛擬機用3個字寬(Word)存盤物件頭,如果物件是非陣列型別,則用2字寬存盤物件頭,在32位虛擬機中,1字寬等于4位元組,即32bit, Java物件頭具體結構描述如下:

Mark Word用于存盤物件自身的運行時資料,如:哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等,32位JVM的Mark Word的默認存盤結構如下:

在運行期間,Mark Word里存盤的資料會隨著鎖標志位的變化而變化,Mark Word可能變化為存盤以下4種資料:

在64位虛擬機下,Mark Word是64bit大小的,其存盤結構如下:

3.2 鎖記錄
在執行緒進入同步代碼塊的時候,如果此同步物件沒有被鎖定,它的鎖標志位是01,則虛擬機首先在當前執行緒的堆疊中創建我們稱之為**“鎖記錄(Lock Record)”**的空間,用于存盤鎖物件的Mark Word的拷貝,官方把這個拷貝稱為Displaced Mark Word,整個Mark Word及其拷貝至關重要,
Lock Record是執行緒私有的資料結構,每一個執行緒都有一個可用Lock Record串列,同時還有一個全域的可用串列,每一個被鎖住的物件Mark Word都會和一個Lock Record關聯(物件頭的Mark Word中的Lock Word指向Lock Record的起始地址),同時Lock Record中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識(或者object mark word),表示該鎖被這個執行緒占用,
| Lock Record | 描述 |
|---|---|
| Owner | 初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖后保存執行緒唯一標識,當鎖被釋放時又設定為NULL, |
| EntryQ | 關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的執行緒, |
| RcThis | 表示blocked或waiting在該monitor record上的所有執行緒的個數, |
| Nest | 用來實作重入鎖的計數, |
| HashCode | 保存從物件頭拷貝過來的HashCode值(可能還包含GC age), |
| Candidate | 用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的背景關系切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降,Candidate只有兩種可能的值0表示沒有需要喚醒的執行緒1表示要喚醒一個繼任執行緒來競爭鎖 |
3.3 Monitor原理
Monitor,常被翻譯為“監視器”或者“管程”,
作業系統在面對行程/執行緒間同步時,所支持的最重要的同步原語即是semaphore 信號量 和 mutex 互斥量,在使用基本的 mutex 進行并發控制時,需要程式員非常小心地控制 mutex 的 down 和 up 操作,否則很容易引起死鎖等問題,為了更容易地撰寫出正確的并發程式,在 mutex 和 semaphore 的基礎上,提出了更高層次的同步原語 Monitor,
不過需要注意的是,作業系統本身并不支持 Monitor機制,Monitor是屬于編程語言的范疇,例如C語言它就不支持 monitor,Java 語言支持 Monitor,Java物件則是天生的Monitor,每一個Java物件都有成為Monitor的“潛質”,這是為什么呢?
因為在Java的設計中,每一個物件自打娘胎里出來,就帶了一把看不見的鎖,通常我們叫**“內部鎖”,或者“Monitor鎖”**,或者“Intrinsic lock”,有了這個鎖的幫助,只要把類的物件方法或者代碼塊用synchronized關鍵字修飾,就會先獲取到與 synchronized 關鍵字系結在一起的 Object 的物件鎖,這個鎖會限定其它執行緒進入與這個鎖相關的synchronized 代碼區域,而這個物件鎖,也就是一個貨真價實的Monitor,
因此,可以把Monitor理解為一種同步工具,也可以理解是一種同步機制,它通常被描述為一個物件,其主要特點有:
- 物件的所有方法都被“互斥”的執行,也就是說,同一個時刻,只有一個 行程/執行緒 能進入 Monitor 中定義的臨界區,
- 通常提供singal機制,即允許正持有“許可”的執行緒暫時放棄“許可”,等待某個謂詞成真(條件變數),而條件成立后,當前行程可以“通知”正在等待這個條件變數的執行緒,讓他可以重新去獲得運行許可,
使用synchronized給物件上鎖時,該物件頭的Mark Word中就被設定為指向Monitor物件的指標,Mark Word鎖標識位為10,其中指標指向的是Monitor物件的起始地址,在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor實作的,其主要資料結構如下(位于HotSpot虛擬機原始碼objectMonitor.hpp檔案,C++實作的):
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0; // 記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 處于wait狀態的執行緒,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 處于等待鎖block狀態的執行緒,會被加入到該串列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
ObjectMonitor中有兩個佇列,_WaitSet 和 _EntryList,用來保存ObjectWaiter物件串列( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件 ),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步代碼:

- 執行緒需要獲取 Object 的鎖時,會被放入 EntrySet(入口區) 中進行等待(enter),
- 如果該執行緒獲取到了鎖(acquire),成為當前鎖的 Owner,
- 如果根據程式邏輯,一個已經獲得了鎖的執行緒缺少某些外部條件,而無法繼續進行下去(例如生產者發現佇列已滿或者消費者發現佇列為空),那么該執行緒可以通過呼叫 wait 方法將鎖釋放(release),進入 Wait Set (等待區)中阻塞(BLOCKED)進行等待,
- 其它執行緒在這個時候有機會獲得鎖,從而使得之前不成立的外部條件成立,這樣先前被阻塞的執行緒就可以重新進入 EntrySet 去競爭鎖(acquire),這個外部條件在 Monitor 機制中稱為條件變數,
Tips:由于進入等待區只有一個入口,由此可以推斷,一個執行緒只有在持有監視器時才能執行wait操作,處于等待的執行緒只有再次獲得監視器才能退出等待狀態,
下面再從位元組碼角度理解一下Monitor原理:
public class Test {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
使用javap 命令反編譯class檔案: javap -v Test.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0 getstatic #2 <com/kai/demo/basic/Test.lock> // <- 取得lock的參考(synchronized開始)
3 dup // 復制了一份lock臨時參考
4 astore_1 // lock臨時參考 -> 存入區域變數表slot 1中
5 monitorenter // 將lock物件的Mark Word置為指向Monitor指標
6 getstatic #3 <com/kai/demo/basic/Test.counter> // <- i
9 iconst_1 // 準備常數1
10 iadd // +1
11 putstatic #3 <com/kai/demo/basic/Test.counter> // -> i
14 aload_1 // <- 取得lock臨時參考,放入運算元堆疊堆疊頂
15 monitorexit // 將lock物件的Mark Word重置,喚醒EntryList
16 goto 24 (+8) // 執行到24行,代碼結束
//下面是例外處理指令,可見,如果出現例外,也能自動地釋放鎖,
19 astore_2 // exception -> slot 2
20 aload_1 // <- 取得lock的參考
21 monitorexit // 將lock物件的Mark Word重置,喚醒EntryList
22 aload_2 // <- slot 2(exception)
23 athrow // throw(exception)
24 return
Exception table:
from to target type
6 16 19 any // 例外檢測6-16行代碼(即臨時區)
19 22 19 any
LineNumberTable:
line 22: 0
line 23: 6
line 24: 14
line 25: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
--- omit ---
執行同步代碼塊后首先要先執行monitorenter指令,退出的時候monitorexit指令,
通過分析之后可以看出,使用Synchronized進行同步,其關鍵就是必須要對物件的監視器monitor進行獲取,當執行緒獲取monitor后才能繼續往下執行,否則就只能等待,而這個獲取的程序是互斥的,即同一時刻只有一個執行緒能夠獲取到monitor,程序大致如下:
如果monitor的進入數為0,則該執行緒進入monitor,然后將進入數設定為1,該執行緒即為monitor的所有者;
如果執行緒已經占有該monitor,如果重新進入,則進入monitor的進入數加1(鎖的重入性);
如果其他執行緒已經占用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權,
monitorexit指令執行時,monitor的進入數減1,如果減1后進入數為0,那執行緒退出monitor,不再是這個monitor的所有者,其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權,
上面案例中,monitorexit指令出現了兩次,第1次為同步正常退出釋放鎖,第2次為發生異步退出釋放鎖,
通過上面兩段描述,我們應該能很清楚的看出Synchronized的實作原理,Synchronized的語意底層是通過一個monitor的物件來完成,
需要注意的是,synchronized用在同步方法上時,位元組碼指令中不會出現monitorenter和monitorexit指令,
例如:
public class Test1 {
public synchronized void method() {
System.out.println("Hello World!");
}
}
使用javap 命令反編譯class檔案:javap -v Test1.class
public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
--- omit ---
方法的同步并沒有通過指令 monitorenter 和 monitorexit 來完成,不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符,JVM就是根據該標示符來實作方法的同步的:
當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor,在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件,
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實作,無需通過位元組碼來完成**,兩個指令的執行是JVM通過呼叫作業系統的互斥原語mutex來實作**,被阻塞的執行緒會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,
4 synchronized進階
4.1 重量級鎖
在JDK 6之前,synchronized通過監視器(Monitor)來實作執行緒同步,但是Monitor本質又是依賴于底層的作業系統的Mutex Lock來實作的,而作業系統要實作執行緒之間的切換需要從用戶態轉換到核心態,轉換時間相對比較長,成本也非常高,因此,后來稱這種鎖為“重量級鎖”,
JDK 6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以,目前鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級,鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖,這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,
四種鎖狀態對應的的Mark Word內容描述如下:

在64位虛擬機下,Mark Word在不同鎖狀態存盤結構如下:

4.2 自旋
4.2.1 自旋鎖
執行緒的阻塞和喚醒需要CPU從用戶態切轉為核心態,而且這種切換不易優化,如果鎖的粒度很小,即鎖持有的時間很短的時候,由鎖競爭造成頻繁地阻塞和喚醒執行緒就顯得非常不值得,因此引入了自旋鎖,
自旋鎖可以減少執行緒阻塞造成的執行緒切換,其執行步驟如下:
- 當前執行緒嘗試去競爭鎖,
- 競爭失敗,準備阻塞自己,
- 但是并沒有阻塞自己,進入自旋狀態(空等待,比如一個空的有限for回圈),
- 自旋狀態下,繼續競爭鎖,
- 如果自旋期間成功獲取鎖,那么結束自旋狀態,否則進入阻塞狀態,
如果在自旋期間成功獲取鎖,那么就減少一次執行緒的切換,
可見,如果持有鎖的執行緒很快就釋放了鎖,那么自旋的效率就非常好,反之,自旋的執行緒就會白白消耗掉處理的資源,所以自旋鎖適合在持有鎖時間短,并且競爭激烈的場景下使用,
在JDK1.6中自旋鎖默認開啟,可以使用-XX:+UseSpinning開啟,-XX:-UseSpinning關閉自旋鎖優化,
自旋的默認次數為10次,可以使用-XX:preBlockSpin引數修改默認的自旋次數,
4.2.2 適應性自旋鎖
適應性自旋,是賦予了自旋一種學習能力,它并不固定自旋10次,他可以根據它前面執行緒的自旋情況,從而調整它的自旋,
例如,執行緒總是自旋成功,那么虛擬機就會允許自旋等待持續的次數更多,反之,如果對于某個鎖,很少有自旋能夠成功,那么在以后競爭這個鎖的時,自旋的次數會減少甚至省略掉自旋程序直接進入阻塞狀態,以免浪費處理器資源,
4.3 鎖消除與鎖粗化
4.3.1 鎖消除
JVM會對不會存在執行緒安全的鎖進行鎖消除,例如使用JDK的內置API,如StringBuffer、Vector、HashTable等會存在隱形的加鎖操作,
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
運行這段代碼時,JVM明顯檢測到變數vector沒有逃逸出方法vectorTest()之外,所以JVM會大膽地將vector內部的加鎖操作消除,
鎖消除的依據是逃逸分析的資料支持,
4.3.2 鎖粗化
在遇到一連串地對同一鎖不斷進行請求和釋放的操作時,JVM會把所有的鎖操作整合成鎖的一次請求,從而減少對鎖的請求同步次數,這個操作叫做鎖的粗化,
例如:
for(int i = 0 ; i < 100 ; i++){
synchronized(lock){
// 同步塊
}
}
鎖粗化后:
synchronized(lock){
for(int i = 0 ; i < 100 ; i++){
// 同步塊
}
}
4.4 偏向鎖
在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了減少此類情況下執行緒獲得鎖的性能消耗,JDK6中引進了偏向鎖,
當一個執行緒訪問同步代碼塊并獲取鎖時,會在Mark Word里存盤鎖偏向的執行緒ID,在執行緒進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word里是否存盤著指向當前執行緒的偏向鎖,引入偏向鎖是為了在沒有多執行緒競爭的情況下盡量減少不必要的輕量級鎖執行路徑,偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令即可,

偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動釋放偏向鎖,偏向鎖的撤銷,需要等待全域安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處于被鎖定狀態,撤銷偏向鎖后恢復到無鎖(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態,
偏向鎖在JDK 6及以后的JVM里是默認啟用的,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之后程式默認會進入輕量級鎖狀態,
下面用OpenJDK 的 JOL 包來做實驗,先添加 maven 依賴:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
定義一個普通的 Java 物件:
public class Person {
String str = "";
Son son = new Son();
}
class Son {
}
利用 JOL 包下的 ClassLayout 來輸出他的記憶體布局:
@Slf4j
public class BiasedLockTest {
public static void main(String[] args) {
Person person = new Person();
log.debug(ClassLayout.parseInstance(person).toPrintable());
}
}
列印結果如下:

圖中的1、2、3、4分別對應 MarkWord、型別指標、實體資料、對齊填充,
- MarkWord:共8位元組,該物件剛新建,鎖標識位是 01,處于無鎖狀態,
- 型別指標:共4位元組,標識新建的 person 物件,
- 實體資料:共8位元組,定義的 Person 有兩個屬性,str 和 son對應的型別分別為 String 和 Son,這兩個屬性各占4個位元組,
- 對齊填充:共4個位元組,前三個部分位元組相加為8+ 4+ 8 = 20,不是8的整數倍,所以得填充4個位元組湊齊24位元組,描述資訊中也有說明:
loss due to the next object aligment,
上面提到,偏向鎖在JDK 6及以后的JVM里是默認啟用的,那為什么啟動后標記位置是“001”無鎖狀態而不是“101”偏向鎖狀態呢?這是因為偏向鎖默認是延遲加載的,不會在程式啟動的時候立刻生效,可以通過JVM引數來避免延遲:-XX:BiasedLockingStartupDelay=0,
再次運行:

4.5 輕量級鎖
偏向鎖多應用只有一個執行緒訪問同步塊場景中,一旦偏向鎖被其他執行緒訪問,就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能,
使用輕量級鎖的多執行緒之間不存在鎖競爭,執行緒是交替執行同步塊的,引入輕量級鎖的目的正是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的性能消耗,
輕量級鎖加鎖程序如下:
- 在代碼塊進入同步塊時,如果同步物件鎖狀態為無鎖狀態(鎖標志位01,是否偏向鎖0),虛擬機首先將在當前執行緒的堆疊幀中建立一個名為鎖記錄(Lock Record)的空間,用于存盤鎖物件目前的Mark Word的拷貝,官方稱之為Displaced Mark Word,
- 拷貝物件頭中的Mark Word復制到鎖記錄中,

- 拷貝成功后,虛擬機將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,并將Lock Record里的owner指標指向物件的Mark Word,

- 如果更新動作成功了,那么這個執行緒就擁有了該物件的鎖,此時物件Mark Word鎖標志位設定為00,表示此物件處于輕量級鎖定狀態,

- 如果更新操作失敗了,虛擬機首先會檢查物件的Mark Word是否指向當前執行緒的堆疊幀,如果是,說明當前執行緒已經擁有了這個物件的鎖,那么可用直接進入同步塊繼續執行,否則說明有多個執行緒競爭鎖,若當前只有一個等待執行緒,則執行緒會通過自旋進行等待;但當自旋超過一定次數或者一個執行緒持有鎖,一個在自旋,又來了第三個執行緒競爭鎖,那么輕量級鎖會膨脹升級為重量級鎖,鎖標志位設定為10,

4.6 鎖膨脹/鎖升級
鎖升級程序:無鎖—>偏向鎖—>輕量級鎖—>重量級鎖,具體如下:

5 簡析CAS
CAS全稱 Compare And Swap(比較與交換),是一種無鎖演算法,在不使用鎖(沒有執行緒被阻塞)的情況下實作多執行緒之間的變數同步,
CAS演算法涉及到三個運算元:
- V 記憶體地址存放的實際值
- A 比較的舊值
- B 更新的新值
當且僅當V的值等于A時(舊值和記憶體中實際的值相同),表明舊值A已經是目前最新版本的值,自然而然可以將新值 N 賦值給 V,反之則表明V和A變數不同步,直接回傳V即可,當多個執行緒使用CAS操作一個變數時,只有一個執行緒會更新成功,其余失敗的執行緒會重新嘗試,也就是說,“更新”是一個不斷重試的操作,
進入原子類AtomicInteger的原始碼,看一下AtomicInteger的定義:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 獲取并操作記憶體的資料,
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 存盤value在AtomicInteger中的偏移量,
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 存盤AtomicInteger的int值,該屬性需要借助volatile關鍵字保證其在執行緒間是可見的,
private volatile int value;
接下來,我們查看AtomicInteger的自增函式incrementAndGet()的原始碼時,發現自增函式底層呼叫的是unsafe.getAndAddInt(),
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
compareAndSwapInt這個函式,它也是CAS縮寫的由來,通過OpenJDK 8 來查看Unsafe.cpp的原始碼:
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
根據OpenJDK 8的原始碼我們可以看出,getAndAddInt()回圈獲取給定物件o中的偏移量處的值v,然后判斷記憶體值是否等于v,如果相等則將記憶體值設定為 v + delta,否則回傳false,繼續回圈進行重試,直到設定成功才能退出回圈,并且將舊值回傳,整個“比較+更新”操作封裝在compareAndSwapInt()中,在JNI里是借助于一個CPU指令完成的,屬于原子操作,可以保證多個執行緒都能夠看到同一個變數的修改值,
后續JDK通過CPU的cmpxchg指令,去比較暫存器中的 A 和 記憶體中的值 V,如果相等,就把要寫入的新值 B 存入記憶體中,如果不相等,就將記憶體值 V 賦值給暫存器中的值 A,然后通過Java代碼中的while回圈再次呼叫cmpxchg指令進行重試,直到設定成功為止,
6 寫在最后
在并行編程程序中,很容易產生執行緒安全問題,比如多執行緒讀寫共享記憶體中的全域變數及靜態變數時引發的競態條件,
我們可以使用synchronized關鍵字來保持執行緒同步,避免上述問題發生,而synchronized是基于Monitor 機制實作的,但是Monitor本質又是依賴于底層的作業系統的Mutex Lock來實作的,而作業系統要實作執行緒之間的切換需要從用戶態轉換到核心態,轉換時間相對比較長,成本也非常高,因此,后來稱這種鎖為“重量級鎖”,
JDK 6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以,目前鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級,性能開銷也逐漸增大,這4種鎖并不是相互替代的關系,它們只是在不同場景下的不同選擇,
| 鎖 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外消耗, 和執行非同步方法相比僅僅存在納秒級的差距, | 執行緒間存在鎖競爭, 會帶來額外的鎖撤銷的消耗, | 適用于只有一個執行緒訪問同步塊場景, |
| 輕量級鎖 | 競爭的執行緒不會阻塞, 提高了執行緒的回應速度, | 如果始終得不到鎖競爭的執行緒, 使用自旋會消耗CPU, | 追求回應速度, 同步塊執行速度非常快, |
| 重量級鎖 | 執行緒競爭不會使用自旋, 不會消耗CPU, | 執行緒阻塞,回應時間緩慢, | 追求吞吐量, 同步塊執行時間較長, |
參考資料
【JAVA學習筆記】多執行緒
JAVA并發編程的藝術
讓你徹底理解Synchronized
Java 中的 Monitor 機制
深入分析Synchronized原理
不可不說的Java“鎖”事
物件的記憶體布局(JOL)和鎖
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/195809.html
標籤:其他
上一篇:螞蟻集團上市創造富神話:員工激勵人均超800萬,可在杭州買個大房子
下一篇:module無法識別的解決方案
