實現synchronized的基礎:
- Java對象頭
- Monitor
Hotspot虛擬機中對象在內存中的佈局:對象頭、實例數據、對齊填充
1、對象頭
主要來說一下對象頭,下圖是對象頭的結構
2、Mark Work
3、Monitor
下面來介紹一下Monitor,Monitor是每個Java對象都持有的一把鎖不過這把鎖是一把看不見的鎖;被稱爲Monitor鎖、管程、監視器鎖。可以將其理解爲一個同步工具,可被描述爲一種同步機制。
synchronized正是通過Monitor來獲取對象的鎖的:
接下來進一步來說一下synchronized底層的字節碼實現原理:
寫一個簡單的同步代碼塊和同步方法使用javac去編譯,然後用javap -verbose去查看編譯出來的.class字節碼文件。
1、首先來看一下同步代碼塊的字節碼:
可以發現Monitor代表獲取到鎖和鎖釋放的過程,中間過程是獲取到對象進行計數,這時使用synchronized獲取到對象的使用權之後就可以重入Monitor。。
重入:從互斥鎖設計上來說當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時候將會進入阻塞狀態,但是當一個線程再次請求自己持有的對象鎖的臨界資源的時候這種情況就屬於重入。
簡單來說就是如果其他的線程先於當前線程獲取到了Monitor使用權當前線程就會被阻塞,Monitor釋放之後又計數器變爲零,其他線程纔有機會重入進來進而去獲取鎖。
2、同步方法的字節碼:
可以看出並沒有Monitor獲取和釋放的過程,是因爲存在一個ACC_SYNCHRONIZED。。。
如果一個方法被其標誌了代表擁有互斥鎖,一次性只能由一個線程去訪問這個同步方法,如果這個方法導致拋出異常就會去釋放鎖,此外都是正常結束之後去釋放鎖。
但是爲什麼有人會對synchronized嗤之以鼻呢???
- 早期版本中synchronized屬於重量級(監視器)鎖,因爲Monitor屬於底層鎖依賴於Mutex Lock因此使用的代價很高;
- 線程之間切換需要從用戶態切換到核心態,開銷很大。
後來Hotspot JVM在JDK6之後進行了很大的提升,對互斥鎖和其他鎖也都進行了很高的性能提升,方法包括:
- Adaptive Spinning 自適應鎖
- Lock Eilminate 鎖消除
- Lock Coarsening 鎖初始化
- Lightweight Locking 輕量級鎖
- Biased Locking 偏斜鎖
4、自旋鎖和自適應鎖
自旋鎖
很多情況下共享數據的鎖定狀態持續時間較短切換線程時不值得的,因此可以使用自旋鎖,不放棄CPU使用的情況下讓線程執行忙循環等待鎖釋放之後再去獲取鎖,但是缺點也是很明顯的如果長時間獲取不到鎖就會帶來很多的性能消耗。
因此爲了改善這些不足,出現了自適應鎖。
5、自適應鎖
JDK6中出現了自適應鎖這代表自旋的次數不再是固定的,是由前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定,比如前一下線程自旋之後獲取到了鎖那意味着現在獲取到鎖的可能性很大會一直自旋,反之會放棄CPU資源之後再請求。
6、鎖消除
是另外一種優化的鎖,優化的更加徹底在JIT編譯的時候對運行的上下文進行掃描去除不可能存在競爭的鎖。
下面是一個簡單的demo:
上面都是關於鎖優化的案例,現在說一個反面的案例:鎖粗化。
7、鎖粗化
在使用互斥鎖時候要讓鎖的範圍儘量小,因爲加鎖和釋放鎖的性能消耗是很大的,但是也存在是希望在循環中加鎖的這就會導致不可避免的麻煩,想到的解決方案就是將整個循環包圍起來避免重複加鎖和釋放鎖。
8、synchronized的四種狀態
現在再來說一個比較關鍵的問題,就是synchronized的四種狀態:無鎖、偏斜鎖、輕量級鎖和重量級鎖。在競爭級別上升的時候鎖的級別也會上升,反之鎖會進行降級,存在閒置的Monitor就會進行降級。
鎖膨脹的方向:無鎖->偏斜鎖->輕量級鎖->重量級鎖
偏斜鎖
大多數情況下鎖不存在多線程競爭,總是由一個線程多次獲得。
核心思想:
如果一個線程獲取到鎖,鎖進行了偏斜模式,此時Mark Word的結構也編程偏斜鎖結構當該線程再次請求鎖的時候不需要再做任何的同步操作,即獲取鎖的過程只需要檢查Mark Word的鎖標記爲爲偏向鎖以及當前線程ID等於Mark Word的Thread ID即可,省去了大量有關鎖的申請操作。
需要注意的是大多數情況下還是同一個線程去申請鎖,實際上synchronized默認級別就是偏斜鎖當鎖競爭激烈的時候鎖會膨脹爲輕量級鎖。
輕量級鎖
輕量級鎖來源於偏斜鎖,當偏斜鎖運行在一個線程進入同步快的情況下,第二個線程加入鎖競爭的時候就會升級了。
適用場景:線程需要交替執行同步快的時候。
偏斜鎖的加鎖過程:
- 代碼進入同步快時候如果同步對象鎖狀態爲無鎖狀態(鎖標誌位01標誌)。虛擬機首先將在當前線程棧幀中建立一個名爲鎖記錄(Lock Record)的空間,同於存儲對象目前的Mark Word的拷貝,官方稱爲Displaced Mark Word。線程堆棧與對象頭的狀態如圖:
- 之後拷貝對象中的Mark Word複製到鎖記錄中;
- 當拷貝成功之後虛擬機會使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock Record中的owner指針指向Object Mark Word。更新成功執行步驟4,否則執行步驟5;
- 如果更新成功,那麼線程擁有了這個對象的鎖,並且對象的Mark Word鎖標誌設置爲00,代表對象處於輕量級鎖狀態,棧幀和對象頭的狀態如圖:
- 如果這個更新失敗了,JVM就會首先檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態變爲10,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。而當前線程便嘗試使用自旋來獲取鎖。
解鎖過程:
- 通過 CAS操作嘗試把線程中複製的DIsplaced Mark Word對象替換當前的Mark Word;
- 如果替換成功,整個同步過程就完成了;
- 如果替換成功,說明有其他線程嘗試獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時喚醒被掛起的線程。