CAS原理 Java SE1.6中的Synchronized

在JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這會致使有鎖(後面的章節還會談到鎖)。 html

鎖機制存在如下問題: java

(1)在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。 算法

(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起。 編程

(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。 數組

volatile是不錯的機制,可是volatile不能保證原子性。所以對於同步最終仍是要回到鎖機制上來。 安全

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。

CAS 操做
多線程

上面的樂觀鎖用到的機制就是CAS,Compare and Swap。 併發

CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。 oracle

非阻塞算法 (nonblocking algorithms) jvm

一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。

現代的CPU提供了特殊的指令,能夠自動更新共享數據,並且可以檢測到其餘線程的干擾,而 compareAndSet() 就用這些代替了鎖定。

拿出AtomicInteger來研究在沒有鎖的狀況下是如何作到數據正確性的。

private volatile int value;

首先毫無覺得,在沒有鎖的機制下可能須要藉助volatile原語,保證線程間的數據是可見的(共享的)。這樣才獲取變量的值的時候才能直接讀取。

public final int get() {
        return value;
    }

而後來看看++i是怎麼作到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。

而compareAndSet利用JNI來完成CPU指令的操做。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

總體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

而整個J.U.C都是創建在CAS之上的,所以對於synchronized阻塞算法,J.U.C在性能上有了很大的提高。

CAS看起來很爽,可是會致使「ABA問題」。

CAS算法實現一個重要前提須要取出內存中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會致使數據的變化

好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。若是鏈表的頭在變化了兩次後恢復了原值,可是不表明鏈表就沒有變化。所以前面提到的原子操做AtomicStampedReference/AtomicMarkableReference就頗有用了。這容許一對變化的元素進行原子操做。


可擴展閱讀:http://ifeve.com/java-synchronized/

本文屬做者原創,原文發表於InfoQ:http://www.infoq.com/cn/articles/java-se-16-synchronized

1 引言

在多線程併發編程中Synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖,可是隨着Java SE1.6對Synchronized進行了各類優化以後,有些狀況下它並不那麼重了,本文詳細介紹了Java SE1.6中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。

2 術語定義

術語 英文 說明
CAS Compare and Swap 比較並設置。用於在硬件層面上提供原子性操做。在 Intel 處理器中,比較並交換經過指令cmpxchg實現。比較是否和給定的數值一致,若是一致則修改,不一致則不修改。

3 同步的基礎

Java中的每個對象均可以做爲鎖。

  • 對於同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前對象的Class對象。
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。那麼鎖存在哪裏呢?鎖裏面會存儲什麼信息呢?

4 同步的原理

JVM規範規定JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另一種方式實現的,細節在JVM規範裏並無詳細說明,可是方法的同步一樣可使用這兩個指令來實現。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每一個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的全部權,即嘗試得到對象的鎖。

4.1 Java對象頭

鎖存在Java對象頭裏。若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等於四字節,即32bit。

長度 內容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等。
32/64bit Class Metadata Address 存儲到對象類型數據的指針
32/64bit Array length 數組的長度(若是當前對象是數組)

Java對象頭裏的Mark Word裏默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構以下:

25 bit 4bit 1bit是不是偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲如下4種數據:

鎖狀態

25 bit

4bit

1bit 2bit
23bit 2bit 是不是偏向鎖 鎖標誌位
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標記 11
偏向鎖 線程ID Epoch 對象分代年齡 1 01

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:

鎖狀態

25bit

31bit

1bit

4bit

1bit 2bit
cms_free 分代年齡 偏向鎖 鎖標誌位
無鎖 unused hashCode 0 01
偏向鎖 ThreadID(54bit) Epoch(2bit) 1 01

4.2 鎖的升級

Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率,下文會詳細分析。

4.3 偏向鎖

Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖,若是測試成功,表示線程已經得到了鎖,若是測試失敗,則須要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),若是沒有設置,則使用CAS競爭鎖,若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,若是線程不處於活動狀態,則將對象頭設置成無鎖狀態,若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

偏向鎖的撤銷

關閉偏向鎖:偏向鎖在Java 6和Java 7裏是默認啓用的,可是它在應用程序啓動幾秒鐘以後才激活,若有必要可使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。若是你肯定本身應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼默認會進入輕量級鎖狀態。

4.4 輕量級鎖

輕量級鎖加鎖:線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

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

由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

5 鎖的優缺點對比

優勢

缺點

適用場景

偏向鎖

加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

競爭的線程不會阻塞,提升了程序的響應速度。

若是始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度很是快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

6 參考源碼

本文一些內容參考了HotSpot源碼 。對象頭源碼markOop.hpp。偏向鎖源碼biasedLocking.cpp。以及其餘源碼ObjectMonitor.cpp和BasicLock.cpp。

7 參考資料

(全文完)若是您喜歡此文請點贊,分享,評論。

相關文章
相關標籤/搜索