【一知半解】synchronied

Hitechr發表於2022-07-12

synchronized是什麼

synchronized是java同步鎖,同一時刻多個執行緒對同一資源進行修改時,能夠保證同一時刻只有一個執行緒獲取到資源並對其進行修改,因此保證了執行緒安全性。
synchronized可以修飾方法和程式碼塊,底層實現的邏輯略有不同。

Object obj=new Object();
synchronized(obj){
    //do soming
}

編譯後的程式碼為:

 ...
10 astore_2
11 monitorenter
12 aload_2
13 monitorexit
14 goto 22 (+8)
17 astore_3
18 aload_2
19 monitorexit
20 aload_3
21 athrow
22 return

當程式碼執行到synchronize(obj)時,對應的位元組碼為monitorenter進行加鎖操作,程式碼執行完後就是monitorexit進行鎖的釋放。兩個 monitorexit是正常退出和異常退出兩種情況下鎖的釋放。

public synchronized void test1(){
  //do somthing
}

當修飾方法時是在編譯後的位元組碼上加上了synchronized的訪問標識

Monitor機制

Monitor是一種同步機制,它的作用是保證同一時刻只有一個執行緒能訪問到受保護的資源,JVM中的同步是基於進入和退出監視物件來實現的,是synchronized的底層實現,每個物件例項都是一個Montor物件,Monitor對應的是底層的MonitorObject,是基於作業系統的互斥mutex實現的。

ObjectMonitor中有幾個關鍵屬性

屬性 描述
_owner 指向持有ObjectMonitor物件的執行緒
_WaitSet 存放處於wait狀態的執行緒佇列
_EntryList 存放處於等待鎖block狀態的執行緒佇列
_recursions 鎖的重入次數
_count 用來記錄該執行緒獲取鎖的次數

  1. 進入monitor,被分配到Entry List中,等待持有鎖的執行緒釋放鎖,
  2. 當執行緒獲取到鎖後,是鎖的持有者,owner指向當前執行緒
  3. 當執行緒進行wait時進入Wait Set,等待鎖的持有者進行喚醒。

synchronized鎖的實現原理

  1. 當程式碼執行到被synchronized修飾的程式碼塊或方法時,首先通過monitor去獲取物件例項的鎖
  2. 當獲取到鎖時,會在物件例項的物件頭上新增鎖標識位
  3. 沒有獲取到鎖的執行緒,會進行到對物件例項的entry list中進行等待
  4. 持有鎖的執行緒的業務處理完後通過修改物件頭上鎖標識位來進行釋放鎖
  5. 當執行緒進行wait操作時,當前也會釋放鎖,然後進行wait set區等待被喚醒
  6. entry list中處理等待的執行緒再次進行鎖的競爭

Mark Word

一個物件的建立要經過這幾步:

  1. 載入:如果物件的Class還沒載入
  2. 連結:由符號引用轉換為地址引用
  3. 初始化:執行Class的方法
  4. 開闢一個地址空間(可以使用TLAB技術進行優化,避免通過CAS產生的資源競爭)
  5. 初始化物件頭資訊
  6. 執行程式碼的方法
    7.返回物件地址
    一個物件有:物件頭例項資料對齊填充三部分組成

    物件頭有:物件標記(Mark Word)型別指標組成,如果物件是陣列,物件頭中還有陣列的長度
    在64位系統中,物件標記佔8個位元組,型別指標佔8個位元組,物件頭共點16個位元組
    物件標記中有hashcode碼GC年齡鎖標記組成

    每個位元組佔8位,8個位元組的MarkWord共佔64位
    無鎖的狀態下,前25位沒有使用,緊接著的32位儲存了物件的hashcode,在1位未使用,後面的4位物件的GC年齡,後面的3位是鎖標記位。

為什麼GC年齡不能超過16

在MarkWord中可以看出GC年齡標記只有4位,二進位制表示就是:1111,對應的十進位制就是15。

下面通過jol進行檢視MarkWord的資訊,

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>

無鎖時

import org.openjdk.jol.info.ClassLayout;
public class MarkWordTest {
    public static void main(String[] args) {
        Hummy hummy=new Hummy();
        int hashCode = hummy.hashCode();
        System.out.println(hashCode);
        System.out.println("二進位制:"+Integer.toBinaryString(hashCode));
        System.out.println("十六進位制: "+Integer.toHexString(hashCode));
        System.out.println(ClassLayout.parseInstance(hummy).toPrintable());
    }
}
class Hummy{}

列印出的結果如下:

可以看到物件的hashcode是:6f496d9f,可以在左邊的Value的找到hashcode值,只不過是反過來的。
最後1位元組的00000001包含了gc年齡和鎖標記位。

加鎖時

import org.openjdk.jol.info.ClassLayout;
public class MarkWordTest {

    public static void main(String[] args) {
        //java -XX:BiasedLockingStartupDelay=0
        Hummy hummy=new Hummy();
        synchronized (hummy){
            System.out.println(ClassLayout.parseInstance(hummy).toPrintable());
        }
    }
}
class Hummy{}


最後一個00000101的最後3位101表示偏向鎖

synchronized的優化

jdk1.6之前只有重量級鎖,面在java1.6之後對synchronized的鎖進行了優化,有偏向鎖、輕量級鎖、重量級鎖,主要是因為重量級鎖需要用到作業系統mutex,作業系統實現執行緒之間的切換需要從使用者態到核心態的,成本非常高。

鎖標識 場景
無鎖 001 不受保護時
偏向鎖 101 只有一個線競爭時
輕量級鎖 00 競爭不激烈時
重量級鎖 10 競爭非常激烈

鎖升級的過程:

  1. 當訪問同步程式碼時,首先判斷markword是否是無鎖狀態(001)或者在偏向鎖狀態下markword中的執行緒id與當前執行緒id是否一樣,如果是則把當前執行緒id通過CAS的方式設定到markword中
  2. 設定成功後則鎖標記修改為(101),升級為偏向當前執行緒的編向鎖(101),執行同步內的方法
  3. 如果失敗,則由jvm進行偏向鎖的撤消
  4. 當持有鎖的執行緒執行到安全點時,檢查偏向鎖的狀態
  5. 當持有鎖的執行緒已退出同步方法時,釋放原執行緒持有的鎖,變成無鎖狀態,到1處執行
  6. 當持有鎖的執行緒還在同步程式碼中,則升級鎖為輕量級鎖(00),當前執行緒持有,另個執行緒通過CAS的方法進行獲取鎖,當自旋到一定次數(20)時,則升級為重量級鎖(10),進入堵塞狀態。