JVM筆記3:線程安全與鎖優化

線程安全

    給“線程安全”下一個嚴謹且可操作的定義:當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果。

Java代碼與JVM實現線程安全

    在編寫Java代碼時,有一些不同的方法保證線程安全,這些方法背後的JVM實現也是不同的。

互斥同步

    互斥同步(Mutual Exclusion & Synchronization)是最常見的一種保證併發正確的手段。同步的意思是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一條(或者一些)線程使用。而互斥是實現同步的方法。

    在Java中實現互斥同步的手段主要有兩種:

  • synchronized關鍵字
  • java.util.concurrent.locks.Lock接口

    其中Lock接口是從JDK5開始支持的,最常見的一種實現爲重入鎖ReentrantLock,其具有一些高級特性:等待可中斷,公平鎖,鎖綁定多個條件。並且在實現之初的效率要高於synchronized關鍵字。不過其並不是Java語言的內置特性,而是類庫實現,JVM很難對其做針對性的優化。

    synchronized關鍵字有兩種使用方法:

  • 同步代碼塊:指定對象參數,那麼鎖定的對象就是指定的對象。
  • 同步方法:如果是實例方法,那麼鎖定的對象就是實例對象;如果是類方法,那麼鎖定的對象就是對應的Class對象

    在JDK1.5版本時,JVM對synchronized關鍵字優化相當有限,只使用了最傳統的鎖機制(現在也稱爲重量級鎖機制)。每個對象都會有一個對應的監視器(monitor)負責監視該對象的上鎖、解鎖過程,JVM在實現時監視器中的操作就可以認爲是直接使用系統調用,從用戶態切換到核心態,利用系統的互斥量實現同步。這個過程中涉及了用戶態、核心態的切換以及線程的阻塞和喚醒,開銷很大,因此監視器也稱爲重量級鎖。這個過程有很大的優化空間,本文的第二部分鎖優化就是JVM針對synchronzied關鍵字的優化。

監視器的實現在JVM中,以HotSpot爲例,objectMonitor的定義與實現在 hotspot/src/share/vm/runtime 路徑下,是C++實現的。等待該鎖的線程會被保存到objectMonitor類實例中的_WaitSet屬性裏,在鎖被釋放時,從_WaitSet中再取出一個等待線程並將其喚醒。喚醒等待線程的順序並不是按照放入的順序,因此監視器實現的是一個非公平鎖。

非阻塞同步

     非阻塞同步(Non-Blocking Synchronization) 指的是訪問共享數據時,先進行操作;如果沒有其他線程爭用共享數據,操作會直接成功,如果有其他的線程在爭用共享數據,那麼再進行其他的補償措施。最常用的補償措施是不斷地重試,直到出現沒有其他線程競爭共享數據爲止。

非阻塞同步是一種樂觀的併發策略,即處理時認爲衝突發生的可能性很小。相對的,互斥同步是一種悲觀的併發策略,其總是認爲只要不去做正確的同步措施就一定會出現問題,無論共享數據是否真的會出現競爭,都會進行加鎖。

    非阻塞同步的實現需要依賴硬件指令集的支持,需要在硬件層面將“衝突檢測”和“操作”兩個步驟封裝爲一個具有原子性的指令。雖然在不同的硬件平臺上有不同的硬件實現,但是在JVM中暴露出來的是統一的CAS(比較並交換,Compare and Swap)操作,通過使用該操作指令能夠實現“嘗試直接操作共享數據”的功能。

    CAS操作需要三個操作數,分別是內存位置(可以簡單理解爲變量的內存地址,用V表示)、預期的舊值(用A表示)和準備設置的新值(用B表示)。CAS指令執行時,當且僅當V中的值符合A時,纔將該位置的值更新爲B,否則該指令不執行。同時,該指令有一條返回值爲V上的舊值。

    在java.util.concurrent(J.U.C)包下的AtomicInteger類的實現就是基於CAS操作的非阻塞同步。如AtomicInteger.getAndIncrement()的實現如下:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

其中unsafe是一個sun.misc.Unsafe類型的對象,該類型可以理解爲jvm爲Java類庫開的一個直通CPU的後門類,可以調用一些很底層的功能。其中的getAndAddInt的實現經過反編譯如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

可以看出,該方法的實現就是依賴於Unsafe.compareAndSwapInt()方法,該方法經過JVM的處理,將會直接翻譯成CPU相關的CAS操作指令。

在JDK5之後,Java類庫中才開始使用CAS操作(在sun.misc.Unsafe類中);在JDK9之後,纔在VarHandle類中開放了面向用戶程序使用的CAS操作

無同步方案

    ThreadLocal類實現了將數據拷貝到線程,該拷貝將只能在線程中訪問。該方法更多依賴於Java類庫的實現,而非JVM的支持。

鎖優化

    JDK6開發了許多鎖優化技術,提高了併發效率。這些技術的總的方向就是在進入同步代碼塊時減少線程切換或者減少重量級鎖的使用。

自旋鎖與自適應自旋

    傳統的synchronized關鍵字的實現中,如果線程在請求鎖時發現已經被其他線程佔用,那麼該線程就會被阻塞從而引起線程的切換,這個過程對性能的影響很大,掛起線程和恢復線程的操作都需要轉入內核態中完成。由此產生了一個優化方向,即這種情況下的線程切換是否可以減少?以下兩個事實支持了該優化方向:

  • 共享數據的鎖定狀態只會持續很短的一段時間,很多時候甚至少於線程切換的時間
  • 現在的計算機很多都是多線程並行的,可以做到持有鎖的線程和等待鎖的線程並行執行

    由此就有了基本的優化方法:讓後面請求鎖的線程“稍等一會”,不放棄處理器資源,而是執行一個空循環,這個空循環就稱爲自旋(Spinning)。自旋的開啓關閉以及自旋的循環次數都可以通過JVM參數設置。

    普通自旋鎖的問題是整個JVM中所有的鎖的自旋次數都是相同的,但是如果一些鎖被佔用的時間很長,自旋鎖反而會浪費處理器資源。在JDK6中對自旋鎖進行了優化,引入了自適應的自旋(Adaptive Spinning),自旋的時間由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。

鎖消除

    鎖消除(Lock Elimination)指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的數據支持。

鎖粗化

    如果有一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁的進行互斥同步操作也會導致不必要的性能損耗,如連續的append操作。
    鎖粗化,就是如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展到整個操作序列的外部,這樣就只需要加鎖一次就夠了。

Mark word

    下面要討論的兩種類型的優化涉及到JVM中對象的內存佈局。在HotSpot虛擬機中,對象頭(Object Header)分爲兩部分,第一部分用於存儲對象自身的運行時數據,鎖相關的數據也保存在該部分,官方稱其爲Mark Word。第二部分用於存儲指向方法區對象類型數據的指針。這裏主要討論Mark Word部分,其在不同狀態下的存儲內容如下:
markword

輕量級鎖

     輕量級鎖(Lightweight Locking) 是JDK6加入的新型鎖機制,是爲了在無需要使用傳統重量級鎖的情況下,提高性能的同時保證同步效果的鎖機制。輕量級鎖機制不使用操作系統互斥量因而效率較高,但是隻能在無實際競爭時起作用,一旦JVM發現某輕量級鎖鎖定的對象上發生競爭,輕量級鎖將膨脹爲傳統的重量級鎖。輕量級鎖的工作過程如下:

  • 在代碼即將計入同步塊時,如果同步對象沒有被鎖定(鎖標誌位爲“01”),虛擬機在棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲目前的Mark Word的拷貝,這份拷貝稱爲Displaced Mark Word。
  • 然後,虛擬機使用CAS操作將對象的Mark Word更新爲指向Lock Record的指針。如果更新成功,表示當前線程獲得了該對象的鎖;否則說明有其他線程在爭用,接下來將膨脹爲重量級鎖,會把Mark Word中存儲的內容變爲指向重量級鎖的的指針,並把標誌爲置爲“10”,等待鎖的線程也將進入阻塞狀態。
  • 持有輕量級鎖的線程在解鎖時,用CAS操作將Displaced Mark Word複製回對象頭。如果成功,則同步過程完成;如果不成功,則說明有其他線程嘗試過獲取該鎖,那麼在釋放鎖的同時需要喚醒掛起的線程。

     總結 :輕量級鎖適用於多個線程交替獲取鎖的場景,即如果沒有競爭,輕量級鎖便通過CAS操作成功避免了使用互斥量的開銷。但是如果確實存在鎖競爭,輕量級鎖反而比重量級鎖更慢。

偏向鎖

    偏向鎖是一種比輕量級鎖更弱的鎖機制,這個鎖會偏向於第一個獲得它的線程,如果在接下來執行的過程中該鎖一直沒有被其他的線程獲取,則持有偏向鎖的線程永遠不需要再進行同步。偏向鎖的執行過程如下:

  • 當鎖對象第一次被線程獲取的時虛擬機會把Mark Word中的標誌爲置爲“01”,偏向模式置爲“1”。同時使用CAS操作將Mark Word的其餘部分填充。如果CAS操作成功,表示當前線程持有了該對象的偏向鎖,以後該線程每次進入這個鎖相關的同步塊時,虛擬機就不需要進行任何同步,持有鎖的線程永遠不會主動釋放。
  • 一旦出現另一個線程去嘗試獲取這個鎖的情況,虛擬機會查看持有該偏向鎖的線程,如果持有鎖的線程目前未鎖定該對象,該對象將恢復到無鎖狀態,進行重偏向。如果持有鎖的線程正鎖定該對象,那麼該偏向鎖膨脹爲輕量級鎖。

     總結 :偏向鎖適用於幾乎只被一個線程訪問的鎖對象。但是如果程序中大多數的鎖都總是被多個不同的線程訪問,那麼偏向模式就是多餘的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章