1. 程式人生 > >「阿里面試系列」分析Synchronized原理,讓面試官仰望

「阿里面試系列」分析Synchronized原理,讓面試官仰望

JAVA架構 2018-12-18 08:01:00

文章簡介

synchronized想必大家都不陌生,用來解決執行緒安全問題的利器。同時也是Java高階程式設計師面試比較常見的面試題。這篇文正會帶大家徹底瞭解synchronized的實現。

擴充套件閱讀:

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

「阿里面試系列」Java執行緒的應用及挑戰

「阿里面試系列」面試加分項,從jvm層面瞭解執行緒的啟動和停止

「阿里面試系列」之前有人問過我一個這樣的面試題

內容導航

  1. 什麼時候需要用Synchronized
  2. synchronized的使用
  3. synchronized的實現原理分析

什麼時候需要用Synchronized

想必大家對synchronized都不陌生,主要作用是在多個執行緒操作共享資料的時候,保證對共享資料訪問的執行緒安全性。

比如在下面這個圖片中,兩個執行緒對於i這個共享變數同時做i++遞增操作,那麼這個時候對於i這個值來說就存在一個不確定性,也就是說理論上i的值應該是2,但是也可能是1。而導致這個問題的原因是執行緒並行執行i++操作並不是原子的,存線上程安全問題。所以通常來說解決辦法是通過加鎖來實現執行緒的序列執行,而synchronized就是java中鎖的實現的關鍵字。

「阿里面試系列」分析Synchronized原理,讓面試官仰望

 

synchronized在併發程式設計中是一個非常重要的角色,在JDK1.6之前,它是一個重量級鎖的角色,但是在JDK1.6之後對synchronized做了優化,優化以後效能有了較大的提升(這塊會在後面做詳細的分析)。

先來看一下synchronized的使用

Synchronized的使用

synchronized有三種使用方法,這三種使用方法分別對應三種不同的作用域,程式碼如下

1. 修飾普通同步方法

將synchronized修飾在普通同步方法,那麼該鎖的作用域是在當前例項物件範圍內,也就是說對於                                               Sync Demosd=new SyncDemo();這一個例項物件sd來說,多個執行緒訪問access方法會有鎖的限制。如果access已經有執行緒持有了鎖,那這個執行緒會獨佔鎖,直到鎖釋放完畢之前,其他執行緒都會被阻塞

public SyncDemo{
 Object lock =new Object();
 //形式1
 public synchronized void access(){
 //
 }
 //形式2,作用域等同於形式1
 public void access1(){
 synchronized(lock){
 //
 }
 }
 //形式3,作用域等同於前面兩種
 public void access2(){
 synchronized(this){
 //
 }
 }
}

2. 修飾靜態同步方法

修飾靜態同步方法或者靜態物件、類,那麼這個鎖的作用範圍是類級別。舉個簡單的例子,

SyncDemo sd=new SyncDemo();
SyncDemo sd2=new SyncDemo();

兩個不同的例項sd和sd2, 如果sd這個例項訪問access方法並且成功持有了鎖,那麼sd2這個物件如果同樣來訪問access方法,那麼它必須要等待sd這個物件的鎖釋放以後,sd2這個物件的執行緒才能訪問該方法,這就是類鎖;也就是說類鎖就相當於全域性鎖的概念,作用範圍是類級別。

這裡拋一個小問題,大家看看能不能回答,如果不能也沒關係,後面會講解;問題是如果sd先訪問access獲得了鎖,sd2物件的執行緒再訪問access1方法,那麼它會被阻塞嗎?

public SyncDemo{
 static Object lock=new Object();
 //形式1
 public synchronized static void access(){
 //
 }
 //形式2等同於形式1
 public void access1(){
 synchronized(lock){
 //
 }
 }
 //形式3等同於前面兩種
 public void access2(){
 synchronzied(SyncDemo.class){
 //
 }
 }
}

 同步方法塊

public SyncDemo{
 Object lock=new Object();
 public void access(){
 //do something
 synchronized(lock){
 //
 }
 }
}

通過演示3種不同鎖的使用,讓大家對synchronized有了初步的認識。當一個執行緒試圖訪問帶有synchronized修飾的同步程式碼塊或者方法時,必須要先獲得鎖。當方法執行完畢退出以後或者出現異常的情況下會自動釋放鎖。如果大家認真看了上面的三個案例,那麼應該知道鎖的範圍控制是由物件的作用域決定的。物件的作用域越大,那麼鎖的範圍也就越大,因此我們可以得出一個初步的猜想,synchronized和物件有非常大的關係。那麼,接下來就去剖析一下鎖的原理

Synchronized的實現原理分析

當一個執行緒嘗試訪問synchronized修飾的程式碼塊時,它首先要獲得鎖,那麼這個鎖到底存在哪裡呢?

物件在記憶體中的佈局

synchronized實現的鎖是儲存在Java物件頭裡,什麼是物件頭呢?在Hotspot虛擬機器中,物件在記憶體中的儲存佈局,可以分為三個區域:物件頭(Header)、例項資料(Instance Data)、對齊填充(Padding)

「阿里面試系列」分析Synchronized原理,讓面試官仰望

 

當我們在Java程式碼中,使用new建立一個物件例項的時候,(hotspot虛擬機器)JVM層面實際上會建立一個 instanceOopDesc物件。

Hotspot虛擬機器採用OOP-Klass模型來描述Java物件例項,OOP(Ordinary Object Point)指的是普通物件指標,Klass用來描述物件例項的具體型別。Hotspot採用instanceOopDesc和arrayOopDesc來描述物件頭,arrayOopDesc物件用來描述陣列型別

instanceOopDesc的定義在Hotspot原始碼中的 instanceOop.hpp檔案中,另外,arrayOopDesc的定義對應 arrayOop.hpp

class instanceOopDesc : public oopDesc {
 public:
 // aligned header size.
 static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
 // If compressed, the offset of the fields of the instance may not be aligned.
 static int base_offset_in_bytes() {
 // offset computation code breaks if UseCompressedClassPointers
 // only is true
 return (UseCompressedOops && UseCompressedClassPointers) ?
 klass_gap_offset_in_bytes() :
 sizeof(instanceOopDesc);
 }
 static bool contains_field_offset(int offset, int nonstatic_field_size) {
 int base_in_bytes = base_offset_in_bytes();
 return (offset >= base_in_bytes &&
 (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
 }
};
#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

從instanceOopDesc程式碼中可以看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot原始碼中的 oop.hpp檔案中

class oopDesc {
 friend class VMStructs;
 private:
 volatile markOop _mark;
 union _metadata {
 Klass* _klass;
 narrowKlass _compressed_klass;
 } _metadata;
 // Fast access to barrier set. Must be initialized.
 static BarrierSet* _bs;
 ...
}

在普通例項物件中,oopDesc的定義包含兩個成員,分別是 _mark和 _metadata

_mark表示物件標記、屬於markOop型別,也就是接下來要講解的Mark World,它記錄了物件和鎖有關的資訊

_metadata表示類元資訊,類元資訊儲存的是物件指向它的類元資料(Klass)的首地址,其中Klass表示普通指標、 _compressed_klass表示壓縮類指標

Mark Word

在前面我們提到過,普通物件的物件頭由兩部分組成,分別是markOop以及類元資訊,markOop官方稱為Mark Word

在Hotspot中,markOop的定義在 markOop.hpp檔案中,程式碼如下

class markOopDesc: public oopDesc {
 private:
 // Conversion
 uintptr_t value() const { return (uintptr_t) this; }
 public:
 // Constants
 enum { age_bits = 4, //分代年齡
 lock_bits = 2, //鎖標識
 biased_lock_bits = 1, //是否為偏向鎖
 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
 hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //物件的hashcode
 cms_bits = LP64_ONLY(1) NOT_LP64(0),
 epoch_bits = 2 //偏向鎖的時間戳
 };
...

Mark word記錄了物件和鎖有關的資訊,當某個物件被synchronized關鍵字當成同步鎖時,那麼圍繞這個鎖的一系列操作都和Mark word有關係。Mark Word在32位虛擬機器的長度是32bit、在64位虛擬機器的長度是64bit。

Mark Word裡面儲存的資料會隨著鎖標誌位的變化而變化,Mark Word可能變化為儲存以下5中情況

「阿里面試系列」分析Synchronized原理,讓面試官仰望

32位虛擬機器中的定義

「阿里面試系列」分析Synchronized原理,讓面試官仰望

64位虛擬機器中的定義

鎖標誌位的表示意義

  1. 鎖標識 lock=00 表示輕量級鎖
  2. 鎖標識 lock=10 表示重量級鎖
  3. 偏向鎖標識 biased_lock=1表示偏向鎖
  4. 偏向鎖標識 biased_lock=0且鎖標識=01表示無鎖狀態

到目前為止,我們再總結一下前面的內容,synchronized(lock)中的lock可以用Java中任何一個物件來表示,而鎖標識的儲存實際上就是在lock這個物件中的物件頭內。大家懂了嗎?

其實前面只提到了鎖標誌位的儲存,但是為什麼任意一個Java物件都能成為鎖物件呢?

首先,Java中的每個物件都派生自Object類,而每個Java Object在JVM內部都有一個native的C++物件 oop/oopDesc進行對應。

其次,執行緒在獲取鎖的時候,實際上就是獲得一個監視器物件(monitor) ,monitor可以認為是一個同步物件,所有的Java物件是天生攜帶monitor.

在hotspot原始碼的 markOop.hpp檔案中,可以看到下面這段程式碼。

ObjectMonitor* monitor() const {
 assert(has_monitor(), "check");
 // Use xor instead of &~ to provide one extra tag-bit check.
 return (ObjectMonitor*) (value() ^ monitor_value);
 }

多個執行緒訪問同步程式碼塊時,相當於去爭搶物件監視器修改物件中的鎖標識,上面的程式碼中ObjectMonitor這個物件和執行緒爭搶鎖的邏輯有密切的關係(後續會詳細分析)

鎖的升級

前面提到了鎖的幾個概念,偏向鎖、輕量級鎖、重量級鎖。在JDK1.6之前,synchronized是一個重量級鎖,效能比較差。從JDK1.6開始,為了減少獲得鎖和釋放鎖帶來的效能消耗,synchronized進行了優化,引入了 偏向鎖和 輕量級鎖的概念。所以從JDK1.6開始,鎖一共會有四種狀態,鎖的狀態根據競爭激烈程度從低到高分別是:無鎖狀態->偏向鎖狀態->輕量級鎖狀態->重量級鎖狀態。這幾個狀態會隨著鎖競爭的情況逐步升級。為了提高獲得鎖和釋放鎖的效率,鎖可以升級但是不能降級。

下面就詳細講解synchronized的三種鎖的狀態及升級原理

偏向鎖

在大多數的情況下,鎖不僅不存在多執行緒的競爭,而且總是由同一個執行緒獲得。因此為了讓執行緒獲得鎖的代價更低引入了偏向鎖的概念。偏向鎖的意思是如果一個執行緒獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他執行緒來競爭鎖,那麼持有偏向鎖的執行緒再次進入或者退出同一個同步程式碼塊,不需要再次進行搶佔鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking開啟或者關閉

偏向鎖的獲取

偏向鎖的獲取過程非常簡單,當一個執行緒訪問同步塊獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存偏向鎖的執行緒ID,表示哪個執行緒獲得了偏向鎖,結合前面分析的Mark Word來分析一下偏向鎖的獲取邏輯

  1. 首先獲取目標物件的Mark Word,根據鎖的標識為和epoch去判斷當前是否處於可偏向的狀態
  2. 如果為可偏向狀態,則通過CAS操作將自己的執行緒ID寫入到MarkWord,如果CAS操作成功,則表示當前執行緒成功獲取到偏向鎖,繼續執行同步程式碼塊
  3. 如果是已偏向狀態,先檢測MarkWord中儲存的threadID和當前訪問的執行緒的threadID是否相等,如果相等,表示當前執行緒已經獲得了偏向鎖,則不需要再獲得鎖直接執行同步程式碼;如果不相等,則證明當前鎖偏向於其他執行緒,需要撤銷偏向鎖。

CAS:表示自旋鎖,由於執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒對CPU來說效能開銷很大。同時,很多物件鎖的鎖定狀態指會持續很短的時間,因此引入了自旋鎖,所謂自旋就是一個無意義的死迴圈,在迴圈體內不斷的重行競爭鎖。當然,自旋的次數會有限制,超出指定的限制會升級到阻塞鎖。

偏向鎖的撤銷

當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放偏向鎖,撤銷偏向鎖的過程需要等待一個全域性安全點(所有工作執行緒都停止位元組碼的執行)。

  1. 首先,暫停擁有偏向鎖的執行緒,然後檢查偏向鎖的執行緒是否為存活狀態
  2. 如果執行緒已經死了,直接把物件頭設定為無鎖狀態
  3. 如果還活著,當達到全域性安全點時獲得偏向鎖的執行緒會被掛起,接著偏向鎖升級為輕量級鎖,然後喚醒被阻塞在全域性安全點的執行緒繼續往下執行同步程式碼

偏向鎖的獲取流程圖

「阿里面試系列」分析Synchronized原理,讓面試官仰望

偏向鎖的獲取流程圖

輕量級鎖

前面我們知道,當存在超過一個執行緒在競爭同一個同步程式碼塊時,會發生偏向鎖的撤銷。偏向鎖撤銷以後物件會可能會處於兩種狀態

  1. 一種是不可偏向的無鎖狀態,簡單來說就是已經獲得偏向鎖的執行緒已經退出了同步程式碼塊,那麼這個時候會撤銷偏向鎖,並升級為輕量級鎖
  2. 一種是不可偏向的已鎖狀態,簡單來說就是已經獲得偏向鎖的執行緒正在執行同步程式碼塊,那麼這個時候會升級到輕量級鎖並且被原持有鎖的執行緒獲得鎖

那麼升級到輕量級鎖以後的加鎖過程和解鎖過程是怎麼樣的呢?

輕量級鎖加鎖

  1. JVM會先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間(LockRecord)
  2. 將物件頭中的Mark Word複製到鎖記錄中,稱為Displaced Mark Word.
  3. 執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標
  4. 如果替換成功,表示當前執行緒獲得輕量級鎖,如果失敗,表示存在其他執行緒競爭鎖,那麼當前執行緒會嘗試使用CAS來獲取鎖,當自旋超過指定次數(可以自定義)時仍然無法獲得鎖,此時鎖會膨脹升級為重量級鎖

「阿里面試系列」分析Synchronized原理,讓面試官仰望

輕量級鎖加鎖

輕量鎖解鎖

  1. 嘗試CAS操作將所記錄中的Mark Word替換回到物件頭中
  2. 如果成功,表示沒有競爭發生
  3. 如果失敗,表示當前鎖存在競爭,鎖會膨脹成重量級鎖

一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於重量級鎖狀態,其他執行緒嘗試獲取鎖時,都會被阻塞,也就是 BLOCKED狀態。當持有鎖的執行緒釋放鎖之後會喚醒這些現場,被喚醒之後的執行緒會進行新一輪的競爭

「阿里面試系列」分析Synchronized原理,讓面試官仰望

輕量級鎖解鎖

重量級鎖

重量級鎖依賴物件內部的monitor鎖來實現,而monitor又依賴作業系統的MutexLock(互斥鎖)

大家如果對MutexLock有興趣,可以抽時間去了解,假設Mutex變數的值為1,表示互斥鎖空閒,這個時候某個執行緒呼叫lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被其他執行緒獲得,其他執行緒呼叫lock只能掛起等待

為什麼重量級鎖的開銷比較大呢?

原因是當系統檢查到是重量級鎖之後,會把等待想要獲取鎖的執行緒阻塞,被阻塞的執行緒不會消耗CPU,但是阻塞或者喚醒一個執行緒,都需要通過作業系統來實現,也就是相當於從使用者態轉化到核心態,而轉化狀態是需要消耗時間的

總結

到目前為止,我們分析了synchronized的使用方法、以及鎖的儲存、物件頭、鎖升級的原理。如果有問題,可以關注我的公眾號:Java架構師學習,或者掃描下方二維碼加群,找我的助理領取視訊資料。群裡有我分享的併發程式設計,分散式,微服務架構,效能優化,原始碼,設計模式,高併發,高可用,Spring,Netty中,Tomcat時,JVM等技術視訊。

擴充套件閱讀:

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

「阿里面試系列」Java執行緒的應用及挑戰

「阿里面試系列」面試加分項,從jvm層面瞭解執行緒的啟動和停止

「阿里面試系列」之前有人問過我一個這樣的面試題