淺析synchronized底層實現與鎖升級過程

在Java中,synchronized關鍵字是用來控制線程同步的。就是在多線程的環境下,控制synchronized代碼段不被多個線程同時執行。java

那麼synchronized具體是怎麼作到線程同步的呢?還有鎖升級過程的過程是怎樣的的?咱們來探討一下。linux

0x01 synchronized實現細節

1.1 Java代碼實現

咱們先來了看下若是多線程間競爭共享資源,不採起措施會出現什麼狀況:安全

public class TestSync implements Runnable {    private int count = 100;    public static void main(String[] args) {
        TestSync ts = new TestSync();
        Thread t1 = new Thread(ts, "線程1");
        Thread t2 = new Thread(ts, "線程2");
        Thread t3 = new Thread(ts, "線程3");

        t1.start();
        t2.start();
        t3.start();
    }    @Override
    public void run() {        while (true) {            if (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            } else {                break;
            }
        }
    }
}複製代碼

線程2將count減到了97,線程三、線程1在某一刻也作了count--,可是結果卻也是97,說明他們在作count--的時候並不知道有別的線程也操做了count。多線程

這個問題,相信你們都知道加synchronized能夠解決。jvm

對run方法做以下修改:ide

@Overridepublic void run() {    while (true) {        synchronized (this) {            if (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            } else {                break;
            }
        }
    }
}複製代碼

執行count--有條不紊,不會出現不安全的問題。工具

所以,在代碼層面,加關鍵字synchronized能解決上述線程安全問題。oop

1.2 字節碼層面如何實現synchronized

若是使用IDEA的話,這裏推薦安裝一個jclasslib Bytecode viewer,這個插件能夠很方便的看程序字節碼執行指令:佈局

咱們來看一下剛纔的程序字節碼指令:測試

實際上synchronized的實現從字節碼層面來看,就是monitorentermonitorexit指令,這兩個就能夠實現synchronized了。

「monitorenter」

Java對象天生就是一個Monitor,當monitor被佔用,它就處於鎖定的狀態。

每一個對象都與一個監視器關聯。且只有在有線程持有的狀況下,監視器才被鎖定。

執行monitorenter的線程嘗試得到monitor的全部權:

  • 若是與objectref關聯的監視器的條目計數爲0,則線程進入監視器,並將其條目計數設置爲1。而後,該線程是monitor的全部者。
  • 若是線程已經擁有與objectref關聯的監視器,則它將從新進入監視器,從而增長其條目計數。這個就是鎖重入。
  • 若是另外一個線程已經擁有與objectref關聯的監視器,則該線程將阻塞,直到該監視器的條目計數爲零爲止,而後再次嘗試獲取全部權。

「monitorexit」

一個或多個MonitorExit指令可與Monitorenter指令一塊兒使用,它們共同實現同步語句。

儘管能夠將monitorentermonitorexit指令用於提供等效的鎖定語義,但它們並未用於同步方法的實現中。

JVM在完成monitorexit時的處理方式分爲正常退出和出現異常時退出:

  • 常規同步方法完成時監視器退出由Java虛擬機的返回指令處理。也就是說程序正常執行完畢的時候,JVM有一個指令會隱式的完成monitor的退出---monitorexit,這個指令是athrow
  • 若是同步語句出現了異常時,JVM的異常處理機制也能monitorexit

簡單的加鎖解鎖過程

所以,執行同步代碼塊後首先要執行monitorenter指令,退出的時候monitorexit指令。

1.3 JVM層實現

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());    synchronized (o) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}複製代碼

執行結果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           08 f3 7f 02 (00001000 11110011 01111111 00000010) (41939720)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total複製代碼

沒有加synchronized的時候,對象頭信息的值爲01 00 00 00,加了鎖以後,對象頭變了08 f3 7f 02,說明synchronized會修改對象的頭新信息,對象頭在Hotspot裏面叫作markword

一個對象的markword裏面有很是重要的信息,其中最重要的就是鎖synchronized。(markword裏還有GC的信息,還有hashcode的信息。)

「Hotspot實現的JVM在64位機的markword信息」

markword信息

0x02 鎖升級過程

2.1 升級過程

在JDK早期的時候,synchronized的底層實現是重量級的,所謂重量級,就是它直接去找操做系統去申請鎖,它的效率是很低的。

JDK後來對synchronized鎖進行了優化,這樣纔有了鎖升級的概念。

鎖升級的過程大體是這樣的:

new -> 「偏向鎖」 -> 「輕量級鎖 (自旋鎖)」-> 「重量級鎖」

synchronized優化的過程和markword息息相關。

用markword中最低的三位表明鎖狀態,其中1位是偏向鎖位,最後兩位是普通鎖位。

  1. Object o = new Object()

鎖 = 0 01 無鎖態

注意:若是偏向鎖打開,默認是匿名偏向狀態

  1. o.hashCode()

001 + hashcode

  1. 默認synchronized(o)

00 -> 輕量級鎖

默認狀況,偏向鎖有個時延,默認是4秒

why? 由於JVM虛擬機本身有一些默認啓動的線程,裏面有好多sync代碼,這些sync代碼啓動時就知道確定會有競爭,若是使用偏向鎖,就會形成偏向鎖不斷的進行鎖撤銷和鎖升級的操做,效率較低。

能夠用BiasedLockingStartupDelay參數設置是否啓動偏向鎖(=0,當即啓動偏向鎖):

-XX:BiasedLockingStartupDelay=0複製代碼
  1. 若是啓動了偏向鎖

鎖升級過程:new Object () - > 101 偏向鎖 ->線程ID爲0 -> Anonymous BiasedLock

打開偏向鎖,new出來的對象,默認就是一個可偏向匿名對象101

  1. 若是有線程上鎖

上偏向鎖,指的就是,把markword的線程ID改成本身線程ID的過程。

偏向鎖不可重偏向、批量偏向、批量撤銷

  1. 若是有線程競爭

撤銷偏向鎖,升級爲輕量級鎖

線程在本身的線程棧生成LockRecord ,用CAS操做將markword設置爲指向本身這個線程的LR的指針,設置成功者獲得鎖

  1. 若是競爭加重

競爭加重:有線程超過10次自旋, (-XX:PreBlockSpin參數可調),或者自旋線程數超過CPU核數的一半, JDK 1.6以後,加入自適應自旋 Adapative Self Spinning ,JVM本身控制。

升級重量級鎖:向操做系統申請資源,linux mutex , CPU從3級-0級系統調用,線程掛起,進入等待隊列,等待操做系統的調度,而後再映射回用戶空間。

總結一下,鎖升級的過程大概是這樣的:

鎖升級過程

2.2 爲何有自旋鎖了還須要重量級鎖

自旋是消耗CPU資源的,若是鎖的時間長,或者自旋線程多,CPU會被大量消耗。

重量級鎖有等待隊列,全部拿不到鎖的進入等待隊列,不須要消耗CPU資源

2.3 偏向鎖是否必定比自旋鎖效率高?

不必定,在明確知道會有多線程競爭的狀況下,偏向鎖確定會涉及鎖撤銷,這時候直接使用自旋鎖。

JVM啓動過程,會有不少線程競爭(明確),因此默認狀況啓動時不打開偏向鎖,過一段兒時間再打開。

2.4 synchronized最底層實現

在硬件層面,鎖實際上是執行了lock cmpxchg xx指令。

synchronized在字節碼層面:

若是鎖的是方法,jvm會加一個synchronized修飾符;

若是是同步代碼快,就是用monitorenter和monitorexit指令。

當jvm看到了synchronized修飾符或者monitorenter和monitorexit的時候,對應的就是C++調用操做系統提供的同步機制。

CPU級別是使用lock指令來實現的。

好比,咱們要在synchronized某一塊內存上設置一個數i,把i的值從0變成1,這個過程放在CPU執行可能會有好幾條指令或者不能同步(速度太快),因此須要有個lock指令。

cmpxchg前面若是加了一個lock的話,後面的指令執行過程當中對這塊區域進行鎖定,只有這條指令能夠修改,其餘指令是不能操做的。

0x03 小結

  • Java對象頭 「markword」

    在Hotspot虛擬機中,對象在內存中的佈局分爲三塊區域:

    • 對象頭
    • 實例數據
    • 對齊填充

Java對象頭是實現synchronized的鎖對象的基礎,通常而言,synchronized使用的鎖對象是存儲在Java對象頭裏。它是輕量級鎖和偏向鎖的關鍵。

  • 「monitor」

一個同步工具,也能夠描述爲一種同步機制。

爲何每一個對象均可以成爲鎖呢? 由於每一個 Java Object 在 JVM 內部都有一個 native 的 C++ 對象 oop/oopDesc 與之對應,而對應的 oop/oopDesc 都會存在一個markOop 對象頭,而這個對象頭是存儲鎖的位置,裏面還有對象監視器,即ObjectMonitor,因此這也是爲何每一個對象都能成爲鎖的緣由之一。

  • 「鎖升級(優化)過程」

synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖;鎖的級別從低到高逐步升級, 無鎖->偏向鎖->輕量級鎖->重量級鎖。

  • 「偏向鎖」

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的markword裏是否存儲着指向當前線程的偏向鎖。

開啓:「-XX:BiasedLockingStartupDelay=0」

  • 「自旋鎖」

自旋等待的時間或者次數是有一個限度的,若是自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起。

JDK1.6中-XX:+UseSpinning開啓; -XX:PreBlockSpin=10 爲自旋次數; JDK1.7後,去掉此參數,由jvm控制。

  • 「重量級鎖」

重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。

相關文章
相關標籤/搜索