天天看點

面試~Synchronized 與 鎖更新

講講 Synchronized/ 講講 Synchronized 鎖更新

内部實作 Markword

synchronized在修飾方法和代碼塊在位元組碼上實作方式有很大差異,但是内部實作還是基于對象頭的MarkWord來實作的。

jdk5 以前 ---重量級鎖

synchronized 隻有重量級鎖,Synchronized是通過對象内部的一個叫做 螢幕鎖 (Monitor)來實作的。

但是 螢幕鎖本質又是依賴于底層的作業系統的 互斥鎖 Mutex Lock來實作的。

并且 作業系統實作線程之間的切換這就需要從使用者态轉換到核心态,這個成本非常高,

狀态之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低`的原因。

是以,這種依賴于作業系統的互斥鎖 Mutex Lock實作的鎖我們稱之為 "重量級鎖"。

JDK6 開始 ---偏向鎖、輕量鎖

對synchronized 進行優化,增加了自适應的 CAS自旋、鎖消除、鎖膨脹、​

​偏向鎖​

​、​

​輕量級鎖​

​ 這些優化政策。

鎖可以從偏向鎖更新到輕量級鎖,再更新的重量級鎖。但是鎖的更新是單向的,也就是說隻能從低到高更新

在 JDK 1.6 中預設是開啟偏向鎖和輕量級鎖的,可以通過-XX:-UseBiasedLocking來禁 用偏向鎖。

偏向鎖

偏向鎖的引進,因為HotSpot作者經過研究實踐發現,

在大多數的情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。

線上程持有偏向鎖之後,後續對同步代碼塊的通路都不需要擷取鎖、釋放鎖。

偏向鎖是在單線程執行代碼塊時使用的機制,如果在多線程并發的環境下(即線程A 尚未執行完同步代碼塊,線程B發起了申請鎖的申請),則一定會轉化為輕量級鎖或者重量級鎖。

"偏向"的意思是,偏向鎖假定将來隻有第一個申請鎖的線程會使用鎖(不會有任何線程再來申請鎖),

是以,隻需要在Mark Word中CAS記錄owner(本質上也是更新,但初始值為空),

如果記錄成功,則偏向鎖擷取成功,記錄鎖狀态為偏向鎖,以後目前線程等于owner就可以零成本的直接獲得鎖;

否則,說明有其他線程競争,膨脹為輕量級鎖。

輕量鎖

引入輕量級鎖的目的其實是為了避免使用重量級鎖。

通過CAS 自旋,避免了短時間内對線程進行阻塞、喚醒,因為線程的阻塞、喚醒對應着作業系統的使用者态和核心态的切換,進而節省資源,提高程式運作的性能。

是以說,各種鎖并不是互相代替的,而是在不同場景下的不同選擇

  • 偏向鎖:無實際競争,且将來隻有第一個申請鎖的線程會使用鎖。
  • 輕量級鎖:無實際競争,多個線程交替使用鎖;允許短時間的鎖競争。
  • 重量級鎖:有實際競争,且激烈競争

synchronized鎖更新過程總結:一句話,就是先自旋,不行再阻塞。

其他必備知識

CAS 自旋

// 執行一個無意義的循環,目的就是等代機會去競争到鎖
while(true){
  //空的
  //一個:每次自旋的時間
  //另外一個:自旋的次數
}      

自旋的作用:

避免了短時間内對線程進行阻塞、喚醒,因為線程的阻塞、喚醒對應着作業系統的使用者态和核心态的切換,進而節省資源,提高程式運作的性能。

鎖資訊存放

面試~Synchronized 與 鎖更新
鎖的資訊是存放在對象頭的 Mark word 中,Mark word 根據鎖的狀态,存儲對應的鎖的資訊

synchronized 與 鎖更新:由對象頭中的 Mark Word 根據 鎖标志位 的不同而被複用及鎖更新政策。

  • 偏向鎖:MarkWord 存儲的是偏向的線程ID;
  • 輕量鎖:MarkWord 存儲的是指向線程​

    ​棧​

    ​中Lock Record的指針;
  • 重量鎖:MarkWord 存儲的是指向 ​

    ​堆​

    ​中的 monitor對象的指針;

鎖的狀态

無鎖狀态

偏向鎖狀态

輕量級鎖狀态

重量級鎖狀态

面試~Synchronized 與 鎖更新

各種鎖的優點、缺點

面試~Synchronized 與 鎖更新

阿裡巴巴開發手冊

【強制】高并發時,同步調用應該去考量 ​

​鎖的性能損耗​

能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。

說明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調用RPC方法。

常見面試題

1、你提到了synchronized 的優化,詳細說一下 偏向鎖、輕量級鎖有什麼差別?

CAS次數不同、是否主動釋放鎖

輕量級鎖每次申請、釋放鎖都至少需要一次CAS,而偏向鎖隻有初始化時需要一次CAS.

偏向鎖,隻偏向于第一個通路的線程。在這個線程(線程A)第一次來通路同步塊時,會使用CAS,更新對象頭的ThreadID為偏向線程的id。後續的通路,隻需要比較threadId是否相同,不需要CAS操作。且這個偏向的線程是不會主動十分鎖的,除非出現線程來競争。

當第二個線程(線程B)過來通路時,他并不知第一個線程已經存在。是以這第二個線程以為它是被偏愛的,它也想向偏向線程那樣使用CAS初始化對象頭的ThreadID,發現失敗了。

說明對象鎖已經被其他線程占用,出現線程競争。

檢查原來持有該對象鎖的線程是否依然存活,

如果挂了,則可以将對象變為無鎖狀态,然後重新偏向新的線程。

如果線程還存活,則檢查線程是否在執行同步代碼塊中的代碼,如果是,則更新為輕量級鎖,進行CAS競争鎖。

輕量級鎖的CAS 是每次申請鎖都需要執行的。

2、你平時是怎麼使用 synchronized 關鍵字

(1) 修飾執行個體方法: 給目前對象執行個體加鎖

synchronized void method() {
 //業務代碼
}      

(2) 修飾靜态⽅法: 也就是給目前類加鎖

synchronized void staic method() {
 //業務代碼
}      

(3) 修飾代碼塊

synchronized(this|object|xx.class) {
 //業務代碼
}      

單例模式的雙重檢索也是使用單例

/* 雙重檢驗鎖 */

public class Singleton{

private Singleton(){}//構造器私有化,防止new,導緻多個執行個體

private static volatile Singleton singleton;

public static Singleton getInstance(){//向外暴露一個靜态的公共方法 getInstance

//第一層檢查

if(singleton == null){

//同步代碼塊

synchronized (Singleton.class){

//第二層檢查

if(singleton == null) {

singleton = new Singleton();

}

}

}

return singleton;

}

}

繼續閱讀