天天看點

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

synchronized是Java多線程中元老級的鎖,也是面試的高頻考點,讓我們來詳細了解synchronized吧。

在Java中,

synchronized

鎖可能是我們最早接觸的鎖了,在 JDK1.5之前synchronized是一個重量級鎖,相對于juc包中的Lock,

synchronized

顯得比較笨重。

慶幸的是在 Java 6 之後 Java 官⽅對從 JVM 層⾯對

synchronized

進行⼤優化,是以現在的 synchronized 鎖效率也優化得很不錯。

一、synchronized 使用

1、synchronized的作用

synchronized

的作用主要有三:

  • (1)、原子性:**所謂原子性就是指一個操作或者多個操作,要麼全部執行并且執行的過程不會被任何因素打斷,要麼就都不執行。**被

    synchronized

    修飾的類或對象的所有操作都是原子的,因為在執行操作之前必須先獲得類或對象的鎖,直到執行完才能釋放。
  • (2)、可見性:**可見性是指多個線程通路一個資源時,該資源的狀态、值資訊等對于其他線程都是可見的。 **synchronized和volatile都具有可見性,其中synchronized對一個類或對象加鎖時,一個線程如果要通路該類或對象必須先獲得它的鎖,而這個鎖的狀态對于其他任何線程都是可見的,并且在釋放鎖之前會将對變量的修改重新整理到共享記憶體當中,保證資源變量的可見性。
  • (3)、有序性:有序性值程式執行的順序按照代碼先後執行。 synchronized和volatile都具有有序性,Java允許編譯器和處理器對指令進行重排,但是指令重排并不會影響單線程的順序,它影響的是多線程并發執行的順序性。synchronized保證了每個時刻都隻有一個線程通路同步代碼塊,也就确定了線程執行同步代碼塊是分先後順序的,保證了有序性。

2、synchronized的使用

Synchronized主要有三種用法:

  • (1)、修飾執行個體方法: 作用于目前對象執行個體加鎖,進入同步代碼前要獲得 目前對象執行個體的鎖
synchronized void method() {
  //業務代碼
}      
  • (2)、修飾靜态方法: 也就是給目前類加鎖,會作用于類的所有對象執行個體 ,進入同步代碼前要獲得 目前 class 的鎖。因為靜态成員不屬于任何一個執行個體對象,是類成員( static 表明這是該類的一個靜态資源,不管 new 了多少個對象,隻有一份)。是以,如果一個線程 A 調用一個執行個體對象的非靜态

    synchronized

    方法,而線程 B 需要調用這個執行個體對象所屬類的靜态

    synchronized

    方法,是允許的,不會發生互斥現象,因為通路靜态

    synchronized

    方法占用的鎖是目前類的鎖,而通路非靜态

    synchronized

    方法占用的鎖是目前執行個體對象鎖。
synchronized void staic method() {
  //業務代碼
}      
  • (3)、修飾代碼塊 :指定加鎖對象,對給定對象/類加鎖。

    synchronized(this|object)

    表示進入同步代碼庫前要獲得給定對象的鎖。

    synchronized(類.class)

    表示進入同步代碼前要獲得 目前 class 的鎖
synchronized(this) {
  //業務代碼
}      

簡單總結一下:

synchronized

關鍵字加到

static

靜态方法和

synchronized(class)

代碼塊上都是是給 Class 類上鎖。

synchronized

關鍵字加到執行個體方法上是給對象執行個體上鎖。

接下來看一個 synchronized 使用經典執行個體—— 線程安全的單例模式:

public class Singleton {
    //保證有序性,防止指令重排
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判斷對象是否已經執行個體過,沒有執行個體化過才進入加鎖代碼
        if (uniqueInstance == null) {
            //類對象加鎖
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}      

二、synchronized同步原理

資料同步需要依賴鎖,那鎖的同步又依賴誰?synchronized給出的答案是在軟體層面依賴JVM,而j.u.c.Lock給出的答案是在硬體層面依賴特殊的CPU指令。

1、synchronized 同步語句塊原理

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代碼塊");
        }
    }
}      

通過 JDK 自帶的

javap

指令檢視

SynchronizedDemo

類的相關位元組碼資訊:首先切換到類的對應目錄執行

javac SynchronizedDemo.java

指令生成編譯後的 .class 檔案,然後執行

javap -c -s -v -l SynchronizedDemo.class

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

從圖中可以看出:

synchronized

同步語句塊的實作使用的是

monitorenter

monitorexit

指令,其中

monitorenter

指令指向同步代碼塊的開始位置,

monitorexit

指令則指明同步代碼塊的結束位置。**

當執行

monitorenter

指令時,線程試圖擷取鎖也就是擷取 對象螢幕

monitor

的持有權。

在 Java 虛拟機(HotSpot)中,Monitor 是基于 C++實作的,由ObjectMonitor實作的。每個對象中都内置了一個

ObjectMonitor

對象。

另外,

wait/notify

等方法也依賴于

monitor

對象,這就是為什麼隻有在同步的塊或者方法中才能調用

wait/notify

等方法,否則會抛出

java.lang.IllegalMonitorStateException

的異常的原因。

在執行

monitorenter

時,會嘗試擷取對象的鎖,如果鎖的計數器為 0 則表示鎖可以被擷取,擷取後将鎖計數器設為 1 也就是加 1。

monitorexit

指令後,将鎖計數器設為 0,表明鎖被釋放。如果擷取對象鎖失敗,那目前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。

2、synchronized 修飾方法原理

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}      

反編譯一下:

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

synchronized

修飾的方法并沒有

monitorenter

指令和

monitorexit

指令,取得代之的确實是

ACC_SYNCHRONIZED

辨別,該辨別指明了該方法是一個同步方法。JVM 通過該

ACC_SYNCHRONIZED

通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。

synchronized

monitorenter

monitorexit

monitorenter

monitorexit

指令則指明同步代碼塊的結束位置。

synchronized

monitorenter

monitorexit

ACC_SYNCHRONIZED

辨別,該辨別指明了該方法是一個同步方法。

不過兩者的本質都是對對象螢幕 monitor 的擷取。

三、synchronized同步概念

1、Java對象頭

在JVM中,對象在記憶體中的布局分為三塊區域:對象頭、執行個體資料和對齊填充。

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

synchronized

用的鎖是存在Java對象頭裡的。

Hotspot 有兩種對象頭:

  • 數組類型,如果對象是數組類型,則虛拟機用3個字寬 (Word)存儲對象頭
  • 非數組類型:如果對象是非數組類型,則用2字寬存儲對象頭。

對象頭由兩部分組成

  • Mark Word:存儲自身的運作時資料,例如 HashCode、GC 年齡、鎖相關資訊等内容。
  • Klass Pointer:類型指針指向它的類中繼資料的指針。

64 位虛拟機 Mark Word 是 64bit,在運作期間,Mark Word裡存儲的資料會随着鎖标志位的變化而變化。

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

2、螢幕(Monitor)

任何一個對象都有一個Monitor與之關聯,當且一個Monitor被持有後,它将處于鎖定狀态。Synchronized在JVM裡的實作都是 基于進入和退出Monitor對象來實作方法同步和代碼塊同步,雖然具體實作細節不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實作。

  1. MonitorEnter指令:插入在同步代碼塊的開始位置,當代碼執行到該指令時,将會嘗試擷取該對象Monitor的所有權,即嘗試獲得該對象的鎖;
  2. MonitorExit指令:插入在方法結束處和異常處,JVM保證每個MonitorEnter必須有對應的MonitorExit;

那什麼是Monitor?可以把它了解為 一個同步工具,也可以描述為 一種同步機制,它通常被 描述為一個對象。

與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java對象自打娘胎裡出來就帶了一把看不見的鎖,它叫做内部鎖或者Monitor鎖。

也就是通常說Synchronized的對象鎖,MarkWord鎖辨別位為10,其中指針指向的是Monitor對象的起始位址。在Java虛拟機(HotSpot)中,Monitor是由ObjectMonitor實作的。

四、synchronized優化

從JDK5引入了現代作業系統新增加的CAS原子操作( JDK5中并沒有對synchronized關鍵字做優化,而是展現在J.U.C中,是以在該版本concurrent包有更好的性能 ),從JDK6開始,就對synchronized的實作機制進行了較大調整,包括使用JDK5引進的CAS自旋之外,還增加了自适應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化政策。由于此關鍵字的優化使得性能極大提高,同時語義清晰、操作簡單、無需手動關閉,是以推薦在允許的情況下盡量使用此關鍵字,同時在性能上此關鍵字還有優化的空間。

鎖主要存在四種狀态,依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态、重量級鎖狀态,鎖可以從偏向鎖更新到輕量級鎖,再更新的重量級鎖。但是鎖的更新是單向的,也就是說隻能從低到高更新,不會出現鎖的降級。

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

1、偏向鎖

偏向鎖是JDK6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。

當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需簡單地測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖。

如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖):如果沒有設定,則使用CAS競争鎖;如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。

偏向鎖使用了一種等到競争出現才釋放鎖的機制,是以當其他線程嘗試競争偏向鎖時, 持有偏向鎖的線程才會釋放鎖。

偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的位元組碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着, 如果線程不處于活動狀态,則将對象頭設定成無鎖狀态;如果線程仍然活着,擁有偏向鎖的棧會被執行,周遊偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向于其他線程,要麼恢複到無鎖或者标記對象不适合作為偏向鎖,最後喚醒暫停的線程。

下圖中的線 程1示範了偏向鎖初始化的流程,線程2示範了偏向鎖撤銷的流程:

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

2、輕量級鎖

引入輕量級鎖的主要目的是 在沒有多線程競争的前提下,減少傳統的重量級鎖使用作業系統互斥量産生的性能消耗。當關閉偏向鎖功能或者多個線程競争偏向鎖導緻偏向鎖更新為輕量級鎖,則會嘗試擷取輕量級鎖。

(1)輕量級鎖加鎖

線程在執行同步塊之前,JVM會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的Mark Word複制到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用 CAS将對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。

(2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作将Displaced Mark Word替換回到對象頭,如果成 功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。

下圖是 兩個線程同時争奪鎖,導緻鎖膨脹的流程圖:

synchronized詳解一、synchronized 使用二、synchronized同步原理三、synchronized同步概念四、synchronized優化

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。當鎖處于這個狀态下,其他線程試圖擷取鎖時, 都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之争。

3、鎖的優缺點比較

各種鎖并不是互相代替的,而是在不同場景下的不同選擇,絕對不是說重量級鎖就是不合适的。每種鎖是隻能更新,不能降級,即由偏向鎖->輕量級鎖->重量級鎖,而這個過程就是開銷逐漸加大的過程。

如果是單線程使用,那偏向鎖毫無疑問代價最小,并且它就能解決問題,連CAS都不用做,僅僅在記憶體中比較下對象頭就可以了;

如果出現了其他線程競争,則偏向鎖就會更新為輕量級鎖;

如果其他線程通過一定次數的CAS嘗試沒有成功,則進入重量級鎖;

鎖的優缺點的對比如下表:

優點 缺點 适用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法僅有納米級的差距 如果線程間存在鎖的競争,會帶來額外的鎖撤銷的消耗 适用于隻有一個線程通路的同步塊場景
輕量級鎖 競争的線程不會阻塞,提高了程式的相應速度 如果始終得不到鎖競争的線程,使用自旋會消耗CPU

追求響應時間

同步響應非常快

重量級鎖 線程競争不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢

追求吞吐量

同步塊執行速度較長