天天看點

談一談對synchronized的了解

Java對象的構成

在 JVM 中,對象在記憶體中分為三塊區域:

對象頭

  • Mark Word(标記字段):預設存儲對象的HashCode,分代年齡和鎖标志位資訊。它會根據對象的狀态複用自己的存儲空間,markword資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32bit和64bit,它的最後2bit是鎖狀态标志位,用來标記目前對象的狀态,對象的所處的狀态,決定了markword存儲的内容,如下表所示:
    談一談對synchronized的了解
    32位虛拟機在不同狀态下markword結構如下圖所示:
    談一談對synchronized的了解
  • Klass Point(類型指針):對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。

執行個體資料

  • 這部分主要是存放類的資料資訊,父類的資訊。

對其填充

  • 由于虛拟機要求對象起始位址必須是8位元組的整數倍,填充資料不是必須存在的,僅僅是為了位元組對齊。
    談一談對synchronized的了解

synchronized加鎖,synchronized 是最常用的線程同步手段之一

synchronized 應用在方法上時,在位元組碼中是通過方法的 ACC_SYNCHRONIZED 标志來實作的。

談一談對synchronized的了解

synchronized 應用在同步塊上時,在位元組碼中是通過 monitorenter 和 monitorexit 實作的。

每個對象都會與一個monitor相關聯,當某個monitor被擁有之後就會被鎖住,當線程執行到monitorenter指令時,就會去嘗試獲得對應的monitor。

步驟如下:

每個monitor維護着一個記錄着擁有次數的計數器。未被擁有的monitor的該計數器為0,當一個線程獲得monitor(執行monitorenter)後,該計數器自增變為 1 。

當同一個線程再次獲得該monitor的時候,計數器再次自增;

當不同線程想要獲得該monitor的時候,就會被阻塞。

當同一個線程釋放 monitor(執行monitorexit指令)的時候,計數器再自減。

當計數器為0的時候,monitor将被釋放,其他線程便可以獲得monitor。

同樣看一下反編譯後的一段鎖定代碼塊的結果:

談一談對synchronized的了解

小結:

同步方法和同步代碼塊底層都是通過monitor來實作同步的。

兩者的差別:同步方式是通過方法中的access_flags中設定ACC_SYNCHRONIZED标志來實作,同步代碼塊是通過monitorenter和monitorexit來實作。

每個對象都與一個monitor相關聯,而monitor可以被線程擁有或釋放。

但是,随着 Java SE 1.6 對 synchronized 進行了各種優化之後,有些情況下它就并不那麼重,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。

針對 synchronized 擷取鎖的方式,JVM 使用了鎖更新的優化方式,就是先使用偏向鎖優先同一線程然後再次擷取鎖,如果失敗,就更新為 CAS 輕量級鎖,如果失敗就會短暫自旋,防止線程被系統挂起。最後如果以上都失敗就更新為重量級鎖。

談一談對synchronized的了解

Linux系統的使用者态和核心态的切換很耗資源,其實就是線程的等待喚起過程,那怎麼才能減少這種消耗呢?

自旋,過來的現在就不斷自旋,防止線程被挂起,一旦可以擷取資源,就直接嘗試成功,直到超出門檻值,自旋鎖的預設大小是10次,-XX:PreBlockSpin可以修改。

自旋都失敗了,那就更新為重量級的鎖,像1.5的一樣,等待喚起咯。

談一談對synchronized的了解

synchronized的執行過程:

  1. 檢測Mark Word裡面是不是目前線程的ID,如果是,表示目前線程處于偏向鎖
  2. 如果不是,則使用CAS将目前線程的ID替換Mard Word,如果成功則表示目前線程獲得偏向鎖,置偏向标志位1
  3. 如果失敗,則說明發生競争,撤銷偏向鎖,進而更新為輕量級鎖。
  4. 目前線程使用CAS将對象頭的Mark Word替換為鎖記錄指針,如果成功,目前線程獲得鎖
  5. 如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。
  6. 如果自旋成功則依然處于輕量級狀态。
  7. 如果自旋失敗,則更新為重量級鎖。

上面幾種鎖都是JVM自己内部實作,當我們執行synchronized同步塊的時候jvm會根據啟用的鎖和目前線程的争用情況,決定如何執行同步操作;

在所有的鎖都啟用的情況下線程進入臨界區時會先去擷取偏向鎖,如果已經存在偏向鎖了,則會嘗試擷取輕量級鎖,啟用自旋鎖,如果自旋也沒有擷取到鎖,則使用重量級鎖,沒有擷取到鎖的線程阻塞挂起,直到持有鎖的線程執行完同步塊喚醒他們;

偏向鎖是在無鎖争用的情況下使用的,也就是同步開在目前線程沒有執行完之前,沒有其它線程會執行該同步塊,一旦有了第二個線程的争用,偏向鎖就會更新為輕量級鎖,如果輕量級鎖自旋到達門檻值後,沒有擷取到鎖,就會更新為重量級鎖;

如果線程争用激烈,那麼應該禁用偏向鎖。