Java併發編程原理與實戰四十二:鎖與volatile的內存語義

鎖與volatile的內存語義

  • 1.鎖的內存語義
  • 2.volatile內存語義
  • 3.synchronized內存語義
  • 4.Lock與synchronized的區別
  • 5.ReentrantLock源碼實例分析

1.鎖的內存語義

鎖是java併發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可讓釋放鎖的線程向獲取同一個鎖的線程發送消息。html

1.1 鎖釋放和獲取的內存語義

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中; 
當線程獲取鎖時,JMM會當前線程擁有的本地內存共享變量置爲無效,從而使得被監視器保護的臨界區代碼必需要從主內存中去讀取共享變量;java

1.2 CAS操做

CAS是單詞compare and set的縮寫,意思是指在set以前先比較該值有沒有變化,只有在沒變的狀況下才對其賦值。編程

問題:如何在沒有鎖的狀況下實現i++原子操做?緩存

CAS操做涉及到三個操做數,一個是內存值,一個是舊的預期值,一個是更新後的值,若是內存值和舊的預期值沒有發生變化,才設置成新的值。多線程

public final int incrementAndGet() {
    for (;;) {
        //獲得預期值
        int current = get();
        //獲得更新後的值
        int next = current + 1;
        //經過CAS操做驗證是否發生變化
        if (compareAndSet(current, next))
            return next;
    }
}

CAS的原子性其實是CPU實現的.併發

CAS操做用途:能夠用CAS在無鎖的狀況下實現原子操做,但要明確應用場合,很是簡單的操做且又不想引入鎖能夠考慮使用CAS操做,當想要非阻塞地完成某一操做也能夠考慮CAS。不推薦在複雜操做中引入CAS,會使程序可讀性變差,且難以測試,同時會出現ABA問題。性能

2.volatile內存語義

2.1 volatile關鍵字的特性:測試

(1)可見性:對一個volatile關鍵字的讀,老是能看到(任意線程)對這個關鍵字的寫

(2)原子性:對任意單個volatile變量的寫操做,具備原子性(注:多個volatile組合操做不具備原子性)
  • 執行volatile寫的時候,JMM會把該線程對應的本地內存(並非實際存在的,也稱爲TLB,線程本地緩衝區)刷新到主內存中。這個過程能夠理解爲線程1(執行寫方法的線程)向接下來要讀取這個變量的線程(執行讀方法的線程)發送了一條消息
  • 執行volatile讀的時候,JMM會把該線程對應的本地內存設置爲無效。線程接下來將直接從主內存中讀取共享變量的值。這個過程能夠理解爲線程2接收到了線程1發送的消息

2.2 內存語義優化

  • 當寫一個volatile變量時,JMM會將本地變量中對應的共享變量值刷新到主內存中
  • 當讀一個volatile變量時,JMM會將線程本地變量存儲的值,置爲無效值,線程接下來將從主內存中讀取共享變量

2.3 實現原理操作系統

instance = new Singleton(); 定義一個volatile變量

其對應編譯後的cpu指令爲:

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

由編譯後的彙編指令能夠看出,改指令相比其餘指令多個一個lock前綴

Lock前綴的指令在多核處理器下會引起了兩件事情:

  • 將當前處理器緩存行中的數據寫回到主內存中
  • 這個寫回的過程會使得其餘處理器中緩存了該內存地址的數據無效

具體實現細節:

  • 鎖總線:早期的實現方式,當CPU讀取共享變量時會鎖住總線,因爲cpu和其餘部件的通訊都是經過總線實現的,若是鎖住總線的話,cpu就不能與其餘部件之間進行通訊,CPU處於等待的狀態,致使整個系統的效率低下。
  • 鎖緩存&緩存一致性協議:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。 

小結:

鎖的內存語義的實現與可重入鎖相關,能夠簡要總結鎖的內存語義的實現包括如下兩種方式:

  • 利用volatile變量的內存語義
  • 利用CAS附帶的volatile語義

3. synchronized內存語義

synchronized也稱爲監視器鎖,由JVM控制實現,每一個對象都有相似監視器同樣的鎖,當監視器鎖被佔用是對象將會處於鎖定狀態,
每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:
  1. 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
  2. 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
  3. 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。

當線程執行monitorexit指令時候,過程以下: 
執行monitorexit的線程必須是objectref所對應的monitor的全部者。

指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。

  經過這兩段描述,咱們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是經過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲何只有在同步的塊或者方法中才能調用wait/notify等方法,不然會拋出java.lang.IllegalMonitorStateException的異常的緣由。 
  

加輕量鎖的過程很簡單:在當前線程的棧幀(stack frame)中生成一個鎖記錄(lock record),這個鎖記錄比前面說的那個對象鎖(管理線程隊列的monitor)簡單多了,它只是對象頭的一個拷貝。而後把對象頭裏的tag改爲00,並把這個棧幀裏的lock record地址放入對象頭裏。若操做成功,那就完成了輕量鎖操做。若是不成功,說明有線程在競爭,則須要在當前對象上生成重量鎖來進行多線程同步,而後將Tag狀態改成10,並生成Monitor對象(重量鎖對象),對象頭裏也會放入Monitor對象的地址。最後將當前線程t排隊隊列中。

輕量鎖的解鎖過程也很簡單就是把棧幀裏剛纔的那個lock record拷貝到對象頭裏,若替換成功,則解鎖完成,若替換不成功,表示在當前線程持有鎖的這段時間內,其餘線程也競爭過鎖,而且發生了鎖升級爲重量鎖,這時須要去Monitor的等待隊列中喚醒一個線程去從新競爭鎖。

4. Lock與synchronized的區別

  1. Lock 擁有Synchronized相同的併發性和內存語義,Lock的實現依賴於cpu級別的指令控制,Synchronized的實現主要由JVM實現控制
  2. synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而Lock在發生異常時,若是沒有主動經過unLock()去釋放鎖,則極可能形成死鎖現象,所以使用Lock時須要在finally塊中釋放鎖;
  3. Lock可讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不可以響應中斷;
  4. 經過Lock能夠知道有沒有成功獲取鎖,而synchronized卻沒法辦到。在性能上來講,若是競爭資源不激烈,二者的性能是差很少的,而當競爭資源很是激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。因此說,在具體使用時要根據適當狀況選擇。
二者在概念上的區別:
1. 二者都是可重入的鎖;
2. synchronized就不是可中斷鎖,而Lock是可中斷鎖;
3. synchronized是非公平鎖,而lock提供公平鎖的實現;
4. Lock提供讀寫兩種鎖操做;

性能比較:

在JDK1.5中,synchronized的性能是比較低的,線程阻塞和喚醒由操做系統內核完成,頻繁的加鎖和放鎖致使頻繁的上下文切換,形成效率低下;所以在多線程環境下,synchronized的吞吐量降低的很是嚴重。但在JDK1.6時對synchronized進行了不少優化,包括偏向鎖、自適應自旋、輕量級鎖等措施。

當須要如下高級特性時,才應該使用Lock:可定時的、可輪詢的與可中斷的鎖獲取操做,公平隊列,或者非塊結構的鎖。不然,請使用synchronized。

ReentrantLock源碼實例分析

https://www.cnblogs.com/pony1223/p/9428248.html

相關文章
相關標籤/搜索