自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

談談JVM內(nèi)部鎖升級過程

開發(fā) 開發(fā)工具 虛擬化
JVM偷懶把任何跟線程有關(guān)的操作全部交給操作系統(tǒng)去做,例如調(diào)度鎖的同步直接交給操作系統(tǒng)去執(zhí)行,而在操作系統(tǒng)中要執(zhí)行先要入隊,另外操作系統(tǒng)啟動一個線程時需要消耗很多資源,消耗資源比較重,重就重在這里。

[[408735]]

一 為什么講這個?

總結(jié)AQS之后,對這方面順帶的復習一下。本文從以下幾個高頻問題出發(fā):

  • 對象在內(nèi)存中的內(nèi)存布局是什么樣的?
  • 描述synchronized和ReentrantLock的底層實現(xiàn)和重入的底層原理。
  • 談談AQS,為什么AQS底層是CAS+volatile?
  • 描述下鎖的四種狀態(tài)和鎖升級過程?
  • Object o = new Object() 在內(nèi)存中占用多少字節(jié)?
  • 自旋鎖是不是一定比重量級鎖效率高?
  • 打開偏向鎖是否效率一定會提升?
  • 重量級鎖到底重在哪里?
  • 重量級鎖什么時候比輕量級鎖效率高,同樣反之呢?

二 加鎖發(fā)生了什么?

無意識中用到鎖的情況:

  1. //System.out.println都加了鎖 
  2. public void println(String x) { 
  3.   synchronized (this) { 
  4.     print(x); 
  5.     newLine(); 
  6.   } 

簡單加鎖發(fā)生了什么?

要弄清楚加鎖之后到底發(fā)生了什么需要看一下對象創(chuàng)建之后再內(nèi)存中的布局是個什么樣的?

一個對象在new出來之后在內(nèi)存中主要分為4個部分:

  • markword這部分其實就是加鎖的核心,同時還包含的對象的一些生命信息,例如是否GC、經(jīng)過了幾次Young GC還存活。
  • klass pointer記錄了指向?qū)ο蟮腸lass文件指針。
  • instance data記錄了對象里面的變量數(shù)據(jù)。
  • padding作為對齊使用,對象在64位服務器版本中,規(guī)定對象內(nèi)存必須要能被8字節(jié)整除,如果不能整除,那么就靠對齊來補。舉個例子:new出了一個對象,內(nèi)存只占用18字節(jié),但是規(guī)定要能被8整除,所以padding=6。

知道了這4個部分之后,我們來驗證一下底層。借助于第三方包 JOL = Java Object Layout java內(nèi)存布局去看看。很簡單的幾行代碼就可以看到內(nèi)存布局的樣式:

  1. public class JOLDemo { 
  2.     private static Object  o; 
  3.     public static void main(String[] args) { 
  4.         o = new Object(); 
  5.         synchronized (o){ 
  6.             System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
  7.         } 
  8.     } 

將結(jié)果打印出來:

從輸出結(jié)果看:

1)對象頭包含了12個字節(jié)分為3行,其中前2行其實就是markword,第三行就是klass指針。值得注意的是在加鎖前后輸出從001變成了000。Markword用處:8字節(jié)(64bit)的頭記錄一些信息,鎖就是修改了markword的內(nèi)容8字節(jié)(64bit)的頭記錄一些信息,鎖就是修改了markword的內(nèi)容字節(jié)(64bit)的頭記錄一些信息。從001無鎖狀態(tài),變成了00輕量級鎖狀態(tài)。

2)New出一個object對象,占用16個字節(jié)。對象頭占用12字節(jié),由于Object中沒有額外的變量,所以instance = 0,考慮要對象內(nèi)存大小要被8字節(jié)整除,那么padding=4,最后new Object() 內(nèi)存大小為16字節(jié)。

拓展:什么樣的對象會進入老年代?很多場景例如對象太大了可以直接進入,但是這里想探討的是為什么從Young GC的對象最多經(jīng)歷15次Young GC還存活就會進入Old區(qū)(年齡是可以調(diào)的,默認是15)。上圖中hotspots的markword的圖中,用了4個bit去表示分代年齡,那么能表示的最大范圍就是0-15。所以這也就是為什么設置新生代的年齡不能超過15,工作中可以通過-XX:MaxTenuringThreshold去調(diào)整,但是一般我們不會動。

三 鎖的升級過程

1 鎖的升級驗證

探討鎖的升級之前,先做個實驗。兩份代碼,不同之處在于一個中途讓它睡了5秒,一個沒睡??纯词欠裼袇^(qū)別。

  1. public class JOLDemo { 
  2.     private static Object  o; 
  3.     public static void main(String[] args) { 
  4.         o = new Object(); 
  5.         synchronized (o){ 
  6.             System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
  7.         } 
  8.     } 
  9. ---------------------------------------------------------------------------------------------- 
  10. public class JOLDemo { 
  11.     private static Object  o; 
  12.     public static void main(String[] args) { 
  13.       try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } 
  14.         o = new Object(); 
  15.         synchronized (o){ 
  16.             System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
  17.         } 
  18.     } 

這兩份代碼會不會有什么區(qū)別?運行之后看看結(jié)果:

有點意思的是,讓主線程睡了5s之后輸出的內(nèi)存布局跟沒睡的輸出結(jié)果居然不一樣。

Syn鎖升級之后,jdk1.8版本的一個底層默認設置4s之后偏向鎖開啟。也就是說在4s內(nèi)是沒有開啟偏向鎖的,加了鎖就直接升級為輕量級鎖了。

那么這里就有幾個問題了?

  • 為什么要進行鎖升級,以前不是默認syn就是重量級鎖么?要么不用要么就用別的不行么?
  • 既然4s內(nèi)如果加了鎖就直接到輕量級,那么能不能不要偏向鎖,為什么要有偏向鎖?
  • 為什么要設置4s之后開始偏向鎖?

問題1:為什么要進行鎖升級?鎖了就鎖了,不就要加鎖么?

首先明確早起jdk1.2效率非常低。那時候syn就是重量級鎖,申請鎖必須要經(jīng)過操作系統(tǒng)老大kernel進行系統(tǒng)調(diào)用,入隊進行排序操作,操作完之后再返回給用戶態(tài)。

內(nèi)核態(tài):用戶態(tài)如果要做一些比較危險的操作直接訪問硬件,很容易把硬件搞死(格式化,訪問網(wǎng)卡,訪問內(nèi)存干掉、)操作系統(tǒng)為了系統(tǒng)安全分成兩層,用戶態(tài)和內(nèi)核態(tài) 。申請鎖資源的時候用戶態(tài)要向操作系統(tǒng)老大內(nèi)核態(tài)申請。Jdk1.2的時候用戶需要跟內(nèi)核態(tài)申請鎖,然后內(nèi)核態(tài)還會給用戶態(tài)。這個過程是非常消耗時間的,導致早期效率特別低。有些jvm就可以處理的為什么還交給操作系統(tǒng)做去呢?能不能把jvm就可以完成的鎖操作拉取出來提升效率,所以也就有了鎖優(yōu)化。

問題2:為什么要有偏向鎖?

其實這本質(zhì)上歸根于一個概率問題,統(tǒng)計表示,在我們?nèi)粘S玫膕yn鎖過程中70%-80%的情況下,一般都只有一個線程去拿鎖,例如我們常使用的System.out.println、StringBuffer,雖然底層加了syn鎖,但是基本沒有多線程競爭的情況。那么這種情況下,沒有必要升級到輕量級鎖級別了。偏向的意義在于:第一個線程拿到鎖,將自己的線程信息標記在鎖上,下次進來就不需要在拿去拿鎖驗證了。如果超過1個線程去搶鎖,那么偏向鎖就會撤銷,升級為輕量級鎖,其實我認為嚴格意義上來講偏向鎖并不算一把真正的鎖,因為只有一個線程去訪問共享資源的時候才會有偏向鎖這個情況。

無意使用到鎖的場景:

  1. /***StringBuffer內(nèi)部同步***/ 
  2. public synchronized int length() { 
  3.   return count
  4. }  
  5.  
  6. //System.out.println 無意識的使用鎖 
  7. public void println(String x) { 
  8.    synchronized (this) { 
  9.      print(x); 
  10.      newLine(); 
  11.    } 
  12.  } 

問題3:為什么jdk8要在4s后開啟偏向鎖?

其實這是一個妥協(xié),明確知道在剛開始執(zhí)行代碼時,一定有好多線程來搶鎖,如果開了偏向鎖效率反而降低,所以上面程序在睡了5s之后偏向鎖才開放。為什么加偏向鎖效率會降低,因為中途多了幾個額外的過程,上了偏向鎖之后多個線程爭搶共享資源的時候要進行鎖升級到輕量級鎖,這個過程還的把偏向鎖進行撤銷在進行升級,所以導致效率會降低。為什么是4s?這是一個統(tǒng)計的時間值。

當然我們是可以禁止偏向鎖的,通過配置參數(shù)-XX:-UseBiasedLocking = false來禁用偏向鎖。jdk15之后默認已經(jīng)禁用了偏向鎖。本文是在jdk8的環(huán)境下做的鎖升級驗證。

2 鎖的升級流程

上面已經(jīng)驗證了對象從創(chuàng)建出來之后進內(nèi)存從無鎖狀態(tài)->偏向鎖(如果開啟了)->輕量級鎖的過程。對于鎖升級的流程繼續(xù)往下,輕量級鎖之后就會變成重量級鎖。首先我們先理解什么叫做輕量級鎖,從一個線程搶占資源(偏向鎖)到多線程搶占資源升級為輕量級鎖,線程如果沒那么多的話,其實這里就可以理解為CAS,也就是我們說的Compare and Swap,比較并交換值。在并發(fā)編程中最簡單的一個例子就是并發(fā)包下面的原子操作類AtomicInteger。在進行類似++操作的時候,底層其實就是CAS鎖。

  1. public class JOLDemo { 
  2.     private static Object  o; 
  3.     public static void main(String[] args) { 
  4.         o = new Object(); 
  5.         synchronized (o){ 
  6.             System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
  7.         } 
  8.     } 
  9. ---------------------------------------------------------------------------------------------- 
  10. public class JOLDemo { 
  11.     private static Object  o; 
  12.     public static void main(String[] args) { 
  13.       try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } 
  14.         o = new Object(); 
  15.         synchronized (o){ 
  16.             System.out.println(ClassLayout.parseInstance(o).toPrintable()); 
  17.         } 
  18.     } 

問題4:什么情況下輕量級鎖要升級為重量級鎖呢?

首先我們可以思考的是多個線程的時候先開啟輕量級鎖,如果它carry不了的情況下才會升級為重量級。那么什么情況下輕量級鎖會carry不住。1、如果線程數(shù)太多,比如上來就是10000個,那么這里CAS要轉(zhuǎn)多久才可能交換值,同時CPU光在這10000個活著的線程中來回切換中就耗費了巨大的資源,這種情況下自然就升級為重量級鎖,直接叫給操作系統(tǒng)入隊管理,那么就算10000個線程那也是處理休眠的情況等待排隊喚醒。2、CAS如果自旋10次依然沒有獲取到鎖,那么也會升級為重量級。

總的來說2種情況會從輕量級升級為重量級,10次自旋或等待cpu調(diào)度的線程數(shù)超過cpu核數(shù)的一半,自動升級為重量級鎖??捶掌鰿PU的核數(shù)怎么看,輸入top指令,然后按1就可以看到。

問題5:都說syn為重量級鎖,那么到底重在哪里?

JVM偷懶把任何跟線程有關(guān)的操作全部交給操作系統(tǒng)去做,例如調(diào)度鎖的同步直接交給操作系統(tǒng)去執(zhí)行,而在操作系統(tǒng)中要執(zhí)行先要入隊,另外操作系統(tǒng)啟動一個線程時需要消耗很多資源,消耗資源比較重,重就重在這里。

整個鎖升級過程如圖所示:

四 synchronized的底層實現(xiàn)

上面我們對對象的內(nèi)存布局有了一些了解之后,知道鎖的狀態(tài)主要存放在markword里面。這里我們看看底層實現(xiàn)。

  1. public class RnEnterLockDemo { 
  2.      public void method() { 
  3.          synchronized (this) { 
  4.              System.out.println("start"); 
  5.          } 
  6.      } 

對這段簡單代碼進行反解析看看什么情況。javap -c RnEnterLockDemo.class

首先我們能確定的是syn肯定是還有加鎖的操作,看到的信息中出現(xiàn)了monitorenter和monitorexit,主觀上就可以猜到這是跟加鎖和解鎖相關(guān)的指令。有意思的是1個monitorenter和2個monitorexit。為什么呢?正常來說應該就是一個加鎖和一個釋放鎖啊。其實這里也體現(xiàn)了syn和lock的區(qū)別。syn是JVM層面的鎖,如果異常了不用自己釋放,jvm會自動幫助釋放,這一步就取決于多出來的那個monitorexit。而lock異常需要我們手動補獲并釋放的。

關(guān)于這兩條指令的作用,我們直接參考JVM規(guī)范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: ? If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. ? If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. ? If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership

翻譯一下:

每個對象有一個監(jiān)視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:

  • 如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設置為1,該線程即為monitor的所有者。
  • 如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1。
  • 如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻譯一下:

執(zhí)行monitorexit的線程必須是objectref所對應的monitor的所有者。指令執(zhí)行時,monitor的進入數(shù)減1,如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor的所有權(quán)。

通過這段話的描述,很清楚的看出Synchronized的實現(xiàn)原理,Synchronized底層通過一個monitor的對象來完成,wait/notify等方法其實也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常。

每個鎖對象擁有一個鎖計數(shù)器和一個指向持有該鎖的線程的指針。

當執(zhí)行monitorenter時,如果目標對象的計數(shù)器為零,那么說明它沒有被其他線程所持有,Java虛擬機會將該鎖對象的持有線程設置為當前線程,并且將其計數(shù)器加i。在目標鎖對象的計數(shù)器不為零的情況下,如果鎖對象的持有線程是當前線程,那么Java虛擬機可以將其計數(shù)器加1,否則需要等待,直至持有線程釋放該鎖。當執(zhí)行monitorexit時,Java虛擬機則需將鎖對象的計數(shù)器減1。計數(shù)器為零代表鎖已被釋放。

總結(jié)

以往的經(jīng)驗中,只要用到synchronized就以為它已經(jīng)成為了重量級鎖。在jdk1.2之前確實如此,后來發(fā)現(xiàn)太重了,消耗了太多操作系統(tǒng)資源,所以對synchronized進行了優(yōu)化。以后可以直接用,至于鎖的力度如何,JVM底層已經(jīng)做好了我們直接用就行。

 

最后再看看開頭的幾個問題,是不是都理解了呢。帶著問題去研究,往往會更加清晰。希望對大家有所幫助。

 

責任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2021-07-06 13:32:55

JVM

2022-03-08 08:44:13

偏向鎖Java內(nèi)置鎖

2021-03-31 10:05:26

偏向鎖輕量級鎖

2023-11-08 08:18:19

鎖升級多線程

2024-06-27 08:55:41

2023-10-07 08:41:42

JavaJVM

2024-04-19 08:05:26

鎖升級Java虛擬機

2012-03-01 10:51:37

JavaJVM

2011-06-17 17:37:16

JavaSQL Server

2011-11-28 12:55:37

JavaJVM

2020-02-24 21:43:36

avaJVM 級鎖線程安全

2020-10-19 09:09:46

Class文件加載過程

2010-09-26 16:14:22

JVM實現(xiàn)機制JVM

2020-12-30 09:18:46

JVM內(nèi)部信息

2011-11-28 12:31:20

JavaJVM

2024-12-03 00:35:20

2019-08-13 15:01:04

變更運維項目經(jīng)理

2024-08-13 14:08:25

2009-07-08 15:11:58

JVM GC調(diào)整優(yōu)化

2021-03-11 08:10:48

JVM對象的創(chuàng)建School
點贊
收藏

51CTO技術(shù)棧公眾號