重學Java——Synchronized底層實現原理

深刻Synchronized底層原理

對於synchronized你們應該都很熟悉,主要做用是在多線程併發時,保證線程訪問共享數據時的線程安全。html

它的做用有三點:java

  1. 確保線程互斥的訪問同步代碼
  2. 保證共享爲師的修改及時可見
  3. 有效解決指令重排(synchronized同步中的代碼,JVM不會輕易優化重排序)

Synchronized使用

它的用法主要是從兩個維度上來區分:安全

  • 根據修飾對象的分類
    • 修飾代碼塊
      • synchronized(this|object)
      • synchronized(類.class)
    • 修飾方法
      • 修飾非靜態方法
      • 修飾靜態方法
  • 根據獲取的鎖來分類
    • 獲取對象鎖
      • synchronized(this|object)
      • 修改非靜態方法
    • 獲取類鎖
      • synchronized(類.class)
      • 修飾靜態方法

1.對象鎖

這個對象是新建的,跟其餘對象無關:數據結構

public class SynchronizeDemo implements Runnable {

    @Override
    public void run() {
        test1();
    }

    private void test1(){
        System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (new SynchronizeDemo()){
            try {
                System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizeDemo sd1 = new SynchronizeDemo();
        Thread thread1 = new Thread(new SynchronizeDemo(),"thread1");
        Thread thread2 = new Thread(new SynchronizeDemo(),"thread2");
        Thread thread3 = new Thread(sd1,"thread3");
        Thread thread4 = new Thread(sd1,"thread4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
複製代碼

運行結果如圖多線程

四個線程同時開始,同時結束,由於做爲鎖的對象與線程是屬於不一樣的實例併發

2.類鎖

無所謂哪一個類,都會被攔截oracle

private void test2(){
        System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (SynchronizeDemo.class){
            try {
                System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
複製代碼

運行結果以下:jvm

能夠看到,類鎖一次只能經過一個。ide

3.this對象鎖

就是把synchronized (SynchronizeDemo.class)改成synchronized (this)工具

控制檯打印結果

可能這顯示結果有點歧義,其實多運行幾回咱們會發現,1和2是同時結束的,3和4永遠有前後,由於3,4同屬於一個實例

4.synchronized修飾方法

private synchronized void test4(){
        ...
    }
複製代碼

打印的結果以下:

thread1_: 22:42:04
thread3_: 22:42:04
thread2_: 22:42:04
thread3_start_: 22:42:04
thread1_start_: 22:42:04
thread2_start_: 22:42:04
thread1_end_: 22:42:06
thread3_end_: 22:42:06
thread2_end_: 22:42:06
thread4_: 22:42:06
thread4_start_: 22:42:06
thread4_end_: 22:42:08
複製代碼

對於非靜態方法,同一個實例的線程訪問會被攔截,非同一實例能夠同時訪問,即此時默認的就是對象鎖(this)

5.修飾靜態方法的結果

在上面方法上加static

thread1_: 22:42:42
thread1_start_: 22:42:42
thread1_end_: 22:42:44
thread4_: 22:42:44
thread4_start_: 22:42:44
thread4_end_: 22:42:46
thread3_: 22:42:46
thread3_start_: 22:42:46
thread3_end_: 22:42:48
thread2_: 22:42:48
thread2_start_: 22:42:48
thread2_end_: 22:42:50
複製代碼

同樣的能夠看出來,靜態方法默認使用的就是類鎖

synchronized使用小結

  • 對於靜態方法,因爲此時對象還沒生成,因此默認採用的就是類鎖(5)
  • 而採用類鎖,就會攔截全部線程,只能讓一個線程訪問(2)
  • 對於對象鎖this,若是是同一實例,那麼按順序執行,若是不是同一實例,就能夠同時訪問(3,4)
  • 若是對象鎖與訪問的對象無關,那麼就會都同時訪問(1)

Synchronized原理

實際上,在JVM中,只區分兩種不一樣的用法,修飾代碼塊與修飾方法,咱們能夠查看SE8規範docs.oracle.com/javase/spec…

(英文很差,我有小助手怕不怕)大意是:Java虛擬機中的同步是經過顯式(經過使用監視器輸入和監視器輸出指令)或隱式(經過方法調用和返回指令)的監視器輸入和退出來實現的。 顯示就是使用monitorenter和monitorexit來控制同步代碼塊;隱式是修飾方法,在運行時常量池中經過ACC_SYNCHRONIZED來標誌。

多說無益,直接看它的字節碼

public class Test {
    public static void main(String[] args) {
    }
    public synchronized void test1() {
    }

    public void test2() {
        synchronized (this) {
        }
    }
}
複製代碼

最簡單的程序,經過使用javap -v Test.class來查看它的字節碼(注意是class文件,不是java文件)

public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LTest;

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter   //監視器進入,獲取鎖
         4: aload_1
         5: monitorexit   //監視器退出,釋放鎖
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
複製代碼

能夠看到,果真字節碼中,synchronized修飾代碼塊時,是使用monitorentermonitorexit來控制,而synchronized修飾方法的時候,是使用ACC_SYNCHRONIZED標識。

本質上都是對一個對象的monitor進行獲取,而這個獲取的過程是排他的,也就是同一時刻只能有一個線程得到同步塊對象的監視器monitor。

線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor全部權,也就是嘗試獲取鎖,執行到monitorexit,也就是釋放全部權,釋放鎖。

要想理清synchronized的鎖的原理,須要掌握兩個重要的概念:

  1. 對象頭
  2. monitor

java對象頭

在Hotspot虛擬機中,對象在內存中的存儲佈局,能夠分爲三塊:對象頭Header,實例數據Instance Data,對齊填充Padding。

Hotspot虛擬機的對象頭包含了兩部分信息:

  1. Mark Word,用於存儲對象自身的運行時數據,好比hash,gc分代年齡,鎖狀態的標誌,線程持有鎖,偏向ID,偏向時間戳等等
  2. Klass Pointer:對象指向它的類的元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。

32位HotSpot虛擬機的對象頭存儲結構以下

img

爲了驗證上圖的正確,咱們能夠查看hotspot的源碼

在線地址hg.openjdk.java.net/jdk8u/jdk8u…

public:
  // Constants
  enum { age_bits                 = 4,//分代年齡
         lock_bits                = 2,//鎖標識
         biased_lock_bits         = 1,//是否偏向鎖
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,//hask
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2//偏向時間戳
  };
複製代碼

hash:保存對象的哈希碼

age:對象的分代年齡

biased_lock:偏向鎖標識位

lock:鎖狀態標識位

JavaThread*:保存持有偏向鎖的線程ID

epoch:保存偏向時間戳

因此,對象頭中的Mark Word,synchronized源碼就是用了對象頭中的Mark Word來標識對象加鎖狀態。

monitor

Monitor Record是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor record關聯(對象頭的MarkWord中的LockWord指向monitor record的起始地址),同時monitor record中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。以下圖所示爲Monitor Record的內部結構

線程惟一標識,當鎖被釋放時又設置爲NULL; EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程。 RcThis:表示blocked或waiting在該monitor record上的全部線程的個數。 Nest:用來實現重入鎖的計數。 HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。

Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭

總結

簡單總結一下,同步塊使用monitorenter和monitorexit指令,而同步方法是依靠方法修飾符上的flag——ACC_SYNCHRONIZED來完成的。其本質都是對一個對象監視器monitor進行獲取,這個獲取過程是排他的,也就是同一時刻只能有一個線程得到由synchronized所保護的對象的監視器。而這個監視器,也能夠理解爲一個同步工具,它是由java對象進行描述的,在Hotspor中,是經過ObjectMonitor來實現,每一個對象中自然都內置了一個ObjectMonitor對象。

在java中,synchronized在編譯後,會在同步塊的先後分別造成一個monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象,若是java程序中明確指定了對象,那就是這個對象的reference,若是沒有指明,那麼根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或者類Class對象來作鎖對象。

在執行monitorenter時,首先會嘗試獲取對象的鎖,若是這個對象沒有鎖,或者當前線程已經擁有了這個對象的鎖,那個鎖的計數器加1,相應的,在執行monitorexit時指令時,會將鎖計數器減1,當計數器爲0時,這個鎖就被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。

擴展

synchronized同步塊對同一線程來講是可重入的,不會出現本身把本身鎖死的狀況,其次,同步塊在已進入的線程執行完成前,會阻塞後面的其餘線程進入。咱們知道,Java的線程是映射到操做系統中的的原生線程上的,若是要阻塞或者喚醒一個線程,都須要操做系統來幫忙,這就須要咱們從用戶態切換到核心態,所以這個狀態轉換是很是耗費CPU。若是這個代碼很是簡單的同步塊,可能切換狀態的時間比代碼執行時間還長。因此synchronized是一個重量級的操做,虛擬機自己也作了大量的優化,引入了偏向鎖,輕量級鎖,重量級鎖等,這一部分鎖的升級,能夠等之後有時間了,再慢慢探討。固然還能夠引入重入鎖,解決synchronized過於重量的問題。


參考

jdk源碼剖析三:鎖Synchronized

Java中synchronized的實現原理與應用

《深刻理解Java虛擬機》


個人CSDN

下面是個人公衆號,歡迎你們關注我

相關文章
相關標籤/搜索