由淺到深分析Synchronized,完全理解Synchronized底層實現原理

這篇文章會記錄Synchronized的經常使用使用場景與Synchronized的底層實現原理。雖然咱們平時常常會在多線程中使用Synchronized關鍵字,但可能對於這個咱們很熟悉的關鍵字的底層究竟是怎樣實現的沒有過多關注。做爲開發者,既然使用到了,能夠試着去一步一步揭開下它的底層面紗。java

爲何要使用Synchronized?

首先咱們來看下這段代碼編程

public class Demo {
    private static int count=0;
    public /*synchronized*/ static void inc(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()-> Demo.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println("運行結果"+count);
    }
}
複製代碼

這段代碼的運行結果:運行結果970。 在這段代碼中,首先沒有加synchronized關鍵字,咱們使用了循環的方法用1000個線程去訪問count這個變量,運行的結果告訴咱們,這個共享變量的狀態是線程不安全的(咱們指望1000次的訪問能夠獲得1000的結果)。要解決這個問題, Synchronized關鍵字就能夠達到目的。安全

synchronized簡介

在多線程併發編程中 synchronized 一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着 Java SE 1.6 對synchronized 進行了各類優化以後,有些狀況下它就並不那麼重,Java SE 1.6 中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。這塊在後續介紹中會慢慢引入。多線程

synchronized的基本語法併發

synchronized 有三種方式來加鎖,分別是jvm

  1. 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖
  2. 靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖
  3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

我在網上找了張圖,大體也對應上面所說的。 佈局

Synchronized的使用場景

synchronized原理分析

Java對象頭和monitor是實現synchronized的基礎!下面就這兩個概念來作詳細介紹。性能

關於monitor,再來看一個小demo

package com.thread;

public class Demo1{

    private static int count = 0;

    public static void main(String[] args) {
        synchronized (Demo1.class) {
            inc();
        }

    }
    private static void inc() {
        count++;
    }
}
複製代碼

上面的代碼demo使用了synchroized關鍵字,鎖住的是類對象。編譯以後,切換到Demo1.class的同級目錄以後,而後用javap -v Demo1.class查看字節碼文件:優化

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/thread/SynchronizedDemo
         2: dup
         3: astore_1
         4: monitorenter       //注意這個
         5: invokestatic  #3                  // Method inc:()V
         8: aload_1
         9: monitorexit    //注意這個
        10: goto          18
        13: astore_2
        14: aload_1
        15: monitorexit   //注意這個
        16: aload_2
        17: athrow
        18: return

複製代碼

線程在獲取鎖的時候,實際上就是得到一個監視器對象(monitor) ,monitor 能夠認爲是一個同步對象,全部的Java 對象是天生攜帶 monitor。而monitor是添加Synchronized關鍵字以後獨有的。synchronized同步塊使用了monitorenter和monitorexit指令實現同步,這兩個指令,本質上都是對一個對象的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器。 線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor全部權,也就是嘗試獲取對象的鎖,而執行monitorexit,就是釋放monitor的全部權。spa

對象在內存中的佈局

在 Hotspot 虛擬機中,對象在內存中的存儲佈局,能夠分爲三個區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。通常而言,synchronized使用的鎖對象是存儲在Java對象頭裏。它是輕量級鎖和偏向鎖的關鍵。

image

Java對象頭

對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。 Klass Point:是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例; Mark Word:用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等,它是實現輕量級鎖和偏向鎖的關鍵.

加鎖時MarkWord可能儲存的四種狀態

synchronized 鎖的升級

在分析 markword 時,提到了偏向鎖、輕量級鎖、重量級鎖。在分析這幾種鎖的區別時,咱們先來思考一個問題使用鎖可以實現數據的安全性,可是會帶來性能的降低。不使用鎖可以基於線程並行提高程序性能,可是卻不能保證線程安全性。這二者之間彷佛是沒有辦法達到既能知足性能也能知足安全性的要求。

hotspot 虛擬機的做者通過調查發現,大部分狀況下,加鎖的代碼不只僅不存在多線程競爭,並且老是由同一個線程屢次得到。因此基於這樣一個機率,是的 synchronized 在JDK1.6 以後作了一些優化,爲了減小得到鎖和釋放鎖來的性能開銷,引入了偏向鎖、輕量級鎖的概念。所以你們會發如今 synchronized 中,鎖存在四種狀態分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖; 鎖的狀態根據競爭激烈的程度從低到高不斷升級。

偏向鎖的基本原理

偏向鎖的獲取 前面說過,大部分狀況下,鎖不只僅不存在多線程競爭,而是老是由同一個線程屢次得到,爲了讓線程獲取鎖的代價更低就引入了偏向鎖的概念。怎麼理解偏向鎖呢?當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當前線程的 ID,後續這個線程進入和退出這段加了同步鎖的代碼塊時,不須要再次加鎖和釋放鎖。而是直接比較對象頭裏面是否存儲了指向當前線程的偏向鎖。若是相等表示偏向鎖是偏向於當前線程的,就不須要再嘗試得到鎖了 偏向鎖的撤銷 偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。而且直接把被偏向的鎖對象升級到被加了輕量級鎖的狀態。 對原持有偏向鎖的線程進行撤銷時,原得到偏向鎖的線程有兩種狀況:

  1. 原得到偏向鎖的線程若是已經退出了臨界區,也就是同步代碼塊執行完了,那麼這個時候會把對象頭設置成無鎖狀態而且爭搶鎖的線程能夠基於 CAS 從新偏向但前線程
  2. 若是原得到偏向鎖的線程的同步代碼塊還沒執行完,處於臨界區以內,這個時候會把原得到偏向鎖的線程升級爲輕量級鎖後繼續執行同步代碼塊在咱們的應用開發中,絕大部分狀況下必定會存在 2 個以上的線程競爭,那麼若是開啓偏向鎖,反而會提高獲取鎖的資源消耗。因此能夠經過 jvm 參數UseBiasedLocking 來設置開啓或關閉偏向鎖

這是網上一張很經典的偏向鎖流程圖

偏向鎖流程圖

輕量級鎖的基本原理

加鎖

鎖升級爲輕量級鎖以後,對象的 Markword 也會進行相應的的變化。升級爲輕量級鎖的過程:

  1. 線程在本身的棧楨中建立鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛建立的鎖記錄中。
  3. 將鎖記錄中的 Owner 指針指向鎖對象。
  4. 將鎖對象的對象頭的 MarkWord替換爲指向鎖記錄的指針。

自旋鎖

輕量級鎖在加鎖過程當中,用到了自旋鎖所謂自旋,就是指當有另一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個得到鎖的線程釋放鎖以後,這個線程就能夠立刻得到鎖的。注意,鎖在原地循環的時候,是會消耗 cpu 的,就至關於在執行一個啥也沒有的 for 循環。因此,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短的時間就可以得到鎖了。自旋鎖的使用,其實也是有必定的機率背景,在大部分同步代碼塊執行的時間都是很短的。因此經過看似無異議的循環反而能提高鎖的性能。可是自旋必要有必定的條件控制,不然若是一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而會消耗 CPU 資源。默認狀況下自旋的次數是 10 次,能夠經過 preBlockSpin 來修改在 JDK1.6 以後,引入了自適應自旋鎖,自適應意味着自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

解鎖

輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

輕量級鎖流程圖

重量級鎖

當輕量級鎖膨脹到重量級鎖以後,意味着線程只能被掛起阻塞來等待被喚醒了。

各類鎖的比較

各類鎖的比較

總結

JVM在運行過程會根據實際狀況對添加了Synchronized關鍵字的部分進行鎖自動升級來實現自我優化。以上就是Synchronized的實現原理和java1.6之後對其所作的優化以及在實際運行中可能遇到的鎖升級原理。雖然你們都懂得使用synchronized這個關鍵字,但我以爲一步一步深刻挖掘它的原理實現的過程也是一種樂趣。

相關文章
相關標籤/搜索