JAVA併發編程的藝術 JMM內存模型

鎖的升級和對比

java1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了"偏向鎖"和"輕量級鎖"。html

偏向鎖

偏向鎖爲了解決大部分狀況下只有一個線程持有鎖的狀況。java

大概邏輯是:每次得到鎖時,在鎖的對象頭信息中存儲了當前線程的ID,下次獲取鎖的時候,不需首先使用CAS競爭鎖,只須要去對象頭裏查看是不是當前線程的ID便可。程序員

ps:其實對象頭中的線程ID應該時刻變化的,不知道細節是怎麼處理的。暫時就瞭解到這一步。再也不深刻學習。數組

輕量級鎖

輕量級鎖使用自旋的CAS獲取鎖。緩存

重量級鎖

重量級鎖使用阻塞來獲取鎖。安全

Java內存模型的抽象結構

在java中,實例域,靜態域,數組元素都是存放在堆內存中的。對內存在線程之間共享。app

局部變量,方法定義參數,異常處理器參數不會再線程之間共享。框架

    線程之間的共享變量存儲在主內存中(Main Memory),每個線程都有本身的本地內存(Local Memory),本地內存中存儲着讀/寫共享變量的副本。ide

由上圖能夠看出,線程之間的通訊由兩個步驟:函數

線程A把修改後的本地內存中的共享變量更新到主內存中去 
線程B到主內存中讀取線程A以前更新過的共享變量

從總體上看,這就是線程A在向線程B發送消息,並且這個消息必須通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲Java程序員提供內存可見性保證。

重排序和內存柵欄

重排序包含兩種:

  • Java編譯器對代碼進行重排序
  • 處理器指令級重排序

一旦咱們使用volatile語義或者synchronize同步,或者鎖,都會限制重排序。

限制重排序是經過內存柵欄實現的。

內存柵欄不只能限制重排序,還能經過刷新每一個CPU的緩存實現可視性。

有四類內存柵欄指令:

屏障類型

指令示例

說明

LoadLoad Barriers

Load1; LoadLoad; Load2

確保Load1數據的裝載,以前於Load2及全部後續裝載指令的裝載。

StoreStore Barriers

Store1; StoreStore; Store2

確保Store1數據對其餘處理器可見(刷新到內存),以前於Store2及全部後續存儲指令的存儲。

LoadStore Barriers

Load1; LoadStore; Store2

確保Load1數據裝載,以前於Store2及全部後續的存儲指令刷新到內存。

StoreLoad Barriers

Store1; StoreLoad; Load2

確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。

happens-before

若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。

happens-before規則以下:

  • 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做(不太明白。個人理解:即便在一個同步塊內部,也會有重排序,應該說是由依賴關係的兩步操做,纔可能會遵循這個規則吧?)。
  • 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
  • 傳遞性:若是A happens- before B,且B happens- before C,那麼A happens- before C。

注:這裏只羅列了4個,其實還有,能夠參考上一篇文章。

注意,兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行(不用緊跟着執行)!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前

as-if-serial

無論怎麼重排序(編譯器和處理器爲了提升並行度),程序的執行結果不能被改變,編譯器,runtime和處理器都必須遵照as-if-serial語義。

爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。

數據依賴關係如:

a = 1;b=a;

或者:

a = 1;a=2;

或者:

a= b;b=1

順序一致性內存模型

這是一種理想化的理論參考模型。順序一致性內存模型進制重排序,每一個操做都必須原子執行並當即對全部線程可見。

JMM實現的的順序一致性:

  • 使用同步:同步塊會串行執行,同步塊內部的代碼可能會重排序。
  • 不適用同步:只能保證最小安全性:線程讀取到的共享值,要麼是以前某個線程寫入的值(可能過時),要麼是初始值。

volatile的內存語義

volatile內衣保證原子性和可見性。

具體實現原理是:

使用內存柵欄,在代碼的相應位置插入相應的內存柵欄指令,能夠作到:

  • 限制重排序:volatile寫不能上一步(任何操做)作重排序 + volatile寫不能和下一步的volatile讀重排序 + volatile讀不能和下一步(任何操做)重排序。(ps:這個地方也不太明白)
  • 緩存:當寫入一個volatile變量時,JMM會把該線程對應的本地內存中的(全部)共享變量刷新到主內存。
  • 緩存:當讀取一個volatile變量時,JMM會清空該線程對應的(全部)本地內存共享變量,那麼讀取的時候只能去主內存中從新讀取。

鎖的內存語義

  • 實現互斥訪問。
  • 緩存:當釋放一個鎖時,JMM會把該線程對應的本地內存中的(全部)共享變量刷新到主內存。
  • 緩存:當得到一個鎖時,JMM會清空該線程對應的(全部)本地內存共享變量,那麼讀取的時候只能去主內存中從新讀取。

鎖的內存語義的實現:

個人理解:鎖是java使用代碼實現的,處理器指令層面沒有鎖的概念。

具體細節我就再也不描述了,大概邏輯是:

AbstractQueuedSynchronizer是一個底層的虛擬類,它內部定義了一個volatile的int屬性(由於是可重入的,這裏是一個計數)。經過對着屬性執行CAS操做實現相似於tryLock的操做。

若是是"輕量級鎖",那麼lock操做應該是自旋tryLock實現的。

若是是"重量級鎖",那麼lock操做應該是在tryLock失敗以後直接掛起,等待被喚醒後重試。(我理解的,掛起和喚醒是操做系統支持的語義)

AbstractQueuedSynchronizer

由於AbstractQueuedSynchronizer比較特殊,在它的基礎上實現了ReentrantLock,ReadWriteLock,以及閉鎖,信號量等。因此這裏把它的API列出來。

AbstractQueuedSynchronizer是一個抽象類,它內部維護了一個volatile類型的int對象,叫state,同時實現了一個不可重寫的final方法compareAndSetState,這個方法提供對state進行原子的CAS操做(應該是native代碼編寫的)。還有兩個一樣final和原子的方法:getState和setState。

AbstractQueuedSynchronizer內部還維護了一個先進先出的隊列。

它的API說明以下:

爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖定和相關同步器(信號量、事件,等等)提供一個框架。

此類的設計目標是成爲依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。

子類必須定義更改此狀態的受保護方法,並定義哪一種狀態對於此對象意味着被獲取或被釋放。假定這些條件以後,此類中的其餘方法就能夠實現全部排隊(經過Queue)和阻塞(經過volatile和CAS)機制

子類能夠維護其餘狀態字段,但只是爲了得到同步而只追蹤使用 getState()setState(int)compareAndSetState(int, int) 方法來操做以原子方式更新的 int 值。

應該將此類定義爲非公共內部幫助器類,可用它們來實現其封閉類的同步屬性。類 AbstractQueuedSynchronizer 沒有實現任何同步接口。而是定義了諸如 acquireInterruptibly(int) 之類的一些方法,在適當的時候能夠經過具體的鎖定和相關同步器來調用它們,以實現其公共方法。

使用

爲了將此類用做同步器的基礎,須要適當地從新定義如下方法,這是經過使用 getState()setState(int) 和/或 compareAndSetState(int, int) 方法來檢查和/或修改同步狀態來實現的:

tryAcquire(int)

tryRelease(int)

tryAcquireShared(int)

tryReleaseShared(int)

isHeldExclusively()

即便此類基於內部的某個 FIFO 隊列,它也沒法強行實施 FIFO 獲取策略。獨佔同步的核心採用如下形式:

Acquire:

while (!tryAcquire(arg)) {

enqueue thread if it is not already queued;

possibly block current thread;

}

 

Release:

if (tryRelease(arg))

unblock the first queued thread;

 

用例

如下是一個非再進入的互斥鎖定類,它使用值 0 表示未鎖定狀態,使用 1 表示鎖定狀態。它還支持一些條件並公開了一個檢測方法:

  1. class Mutex implements Lock, java.io.Serializable {
  2.  
  3.     // Our internal helper class
  4.     private static class Sync extends AbstractQueuedSynchronizer {
  5.       // Report whether in locked state
  6.       protected boolean isHeldExclusively() {
  7.         return getState() == 1;
  8.       }
  9.  
  10.       // Acquire the lock if state is zero
  11.       public boolean tryAcquire(int acquires) {
  12.         assert acquires == 1; // Otherwise unused
  13.         return compareAndSetState(0, 1);
  14.       }
  15.  
  16.       // Release the lock by setting state to zero
  17.       protected boolean tryRelease(int releases) {
  18.         assert releases == 1; // Otherwise unused
  19.         if (getState() == 0) throw new IllegalMonitorStateException();
  20.         setState(0);
  21.         return true;
  22.       }
  23.  
  24.       // Provide a Condition
  25.       Condition newCondition() { return new ConditionObject(); }
  26.  
  27.       // Deserialize properly
  28.       private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
  29.         s.defaultReadObject();
  30.         setState(0); // reset to unlocked state
  31.       }
  32.     }
  33.  
  34.     // The sync object does all the hard work. We just forward to it.
  35.     private final Sync sync = new Sync();
  36.  
  37.     public void lock() { sync.acquire(1); }
  38.     public boolean tryLock() { return sync.tryAcquire(1); }
  39.     public void unlock() { sync.release(1); }
  40.     public Condition newCondition() { return sync.newCondition(); }
  41.     public boolean isLocked() { return sync.isHeldExclusively(); }
  42.     public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
  43.     public void lockInterruptibly() throws InterruptedException {
  44.       sync.acquireInterruptibly(1);
  45.     }
  46.     public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
  47.       return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  48.     }
  49.  }

如下是一個鎖存器類,它相似於 CountDownLatch,除了只須要觸發單個 signal 以外。由於鎖存器是非獨佔的,因此它使用 shared 的獲取和釋放方法。

  1. class BooleanLatch {
  2.  
  3.    private static class Sync extends AbstractQueuedSynchronizer {
  4.      boolean isSignalled() { return getState() != 0; }
  5.  
  6.      protected int tryAcquireShared(int ignore) {
  7.        return isSignalled()? 1 : -1;
  8.      }
  9.  
  10.      protected boolean tryReleaseShared(int ignore) {
  11.        setState(1);
  12.        return true;
  13.      }
  14.    }
  15.  
  16.    private final Sync sync = new Sync();
  17.    public boolean isSignalled() { return sync.isSignalled(); }
  18.    public void signal() { sync.releaseShared(1); }
  19.    public void await() throws InterruptedException {
  20.      sync.acquireSharedInterruptibly(1);
  21.    }
  22. }

讀寫鎖的視線分析

接下來將分析ReentrantReadWriteLock的實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級(如下沒有特別說明讀寫鎖都可認爲是ReentrantReadWriteLock)。

讀寫狀態的設計

讀寫鎖一樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器須要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫鎖實現的關鍵。

若是在一個整型變量上維護多種狀態,就必定須要"按位切割使用"這個變量,讀寫鎖是將變量切分紅了兩個部分,高16位表示讀,低16位表示寫,劃分方式如圖1所示。

圖1. 讀寫鎖狀態的劃分方式

        如圖1所示,當前同步狀態表示一個線程已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速的肯定讀和寫各自的狀態呢?答案是經過位運算。假設當前同步狀態值爲S,寫狀態等於 S & 0x0000FFFF(將高16位所有抹去),讀狀態等於 S >>> 16(無符號補0右移16位)。當寫狀態增長1時,等於S + 1,當讀狀態增長1時,等於S + (1 << 16),也就是S + 0x00010000。

根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S & 0x0000FFFF)等於0時,則讀狀態(S >>> 16)大於0,即讀鎖已被獲取。

寫鎖的獲取與釋放

寫鎖是一個支持重進入的排它鎖。若是當前線程已經獲取了寫鎖,則增長寫狀態。若是當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態,獲取寫鎖的代碼以下:

  1. protected final boolean tryAcquire(int acquires) {
  2.    Thread current = Thread.currentThread();
  3.    int c = getState();
  4.    int w = exclusiveCount(c);
  5.    if (c != 0) {
  6.       // 存在讀鎖或者當前獲取線程不是已經獲取寫鎖的線程
  7.       if (w == 0 || current != getExclusiveOwnerThread())
  8.          return false;
  9.       if (w + exclusiveCount(acquires) > MAX_COUNT)
  10.          throw new Error("Maximum lock count exceeded");
  11.       setState(c + acquires);
  12.       return true;
  13.    }
  14.    if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
  15.       return false;
  16.    }
  17.    setExclusiveOwnerThread(current);
  18.    return true;
  19. }

 

該方法除了重入條件(當前線程爲獲取了寫鎖的線程)以外,增長了一個讀鎖是否存在的判斷。若是存在讀鎖,則寫鎖不能被獲取,緣由在於:讀寫鎖要確保寫鎖的操做對讀鎖可見,若是容許讀鎖在已被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程就沒法感知到當前寫線程的操做。所以只有等待其餘讀線程都釋放了讀鎖,寫鎖才能被當前線程所獲取,而寫鎖一旦被獲取,則其餘讀寫線程的後續訪問均被阻塞。

寫鎖的釋放與ReentrantLock的釋放過程基本相似,每次釋放均減小寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程可以繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。

讀鎖的獲取與釋放

讀鎖是一個支持重進入的共享鎖,它可以被多個線程同時獲取,在沒有其餘寫線程訪問(或者寫狀態爲0)時,讀鎖總會成功的被獲取,而所作的也只是(線程安全的)增長讀狀態。若是當前線程已經獲取了讀鎖,則增長讀狀態。若是當前線程在獲取讀鎖時,寫鎖已被其餘線程獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要緣由是新增了一些功能,好比:getReadHoldCount()方法,返回當前線程獲取讀鎖的次數。讀狀態是全部線程獲取讀鎖次數的總和,而每一個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。所以,這裏將獲取讀鎖的代碼作了刪減,保留必要的部分,代碼以下。

  1. protected final int tryAcquireShared(int unused) {
  2.    for (;;) {
  3.       int c = getState();
  4.       int nextc = c + (1 << 16);
  5.       if (nextc < c)
  6.          throw new Error("Maximum lock count exceeded");
  7.       if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
  8.          return -1;
  9.       if (compareAndSetState(c, nextc))
  10.          return 1;
  11.    }
  12. }

在tryAcquireShared(int unused)方法中,若是其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。若是當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增長讀狀態,成功獲取讀鎖。

讀鎖的每次釋放均(線程安全的,可能有多個讀線程同時釋放讀鎖)減小讀狀態,減小的值是(1 << 16)。

 

final域的內存語義

  • JMM禁止編譯器把final域的寫重排序到構造函數以外(個人理解:也就是說非final域可能發生這種狀況,致使其餘線程看到未徹底構造完成的對象)。
  • 第一次讀取final域的對象的引用,在順序上必須是第一次讀取,不能跟隨後的第二次第三次重排序。(個人理解:我懷疑第一次可能涉及初始化這個final域,可是final域明明是在構造函數以前初始化的的,那麼就應該是構造函數內部,也可能屢次讀取final域)

具體的緣由和實現書上有,我不具體研究了。

 

雙重檢查鎖定和延遲初始化

上一篇講過這個問題。不過這本書給了另一種解決方案:

基於類初始化的解決方案

JVM在類(Class,而不是對象)的初始化階段(即在Class被加載後,且被線程使用以前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖能夠同步多個線程對同一個類的初始化。

基於這個特性,能夠實現另外一種線程安全的延遲初始化方案(這個方案被稱之爲Initialization On Demand Holder idiom):

  1. public class InstanceFactory {
  2.     private static class InstanceHolder {
  3.         public static Instance instance = new Instance();
  4.     }
  5.  
  6.     public static Instance getInstance() {
  7.         return InstanceHolder.instance ; //這裏將致使InstanceHolder類被初始化
  8.     }
  9. }

個人理解:其實這種方式是在初始化階段加鎖一次實現的,而使用volatile的雙重檢查鎖定通常狀況下,也只會加鎖一次(由於第二次進入,if會成功,就不進入synchronize塊了)。在性能上,他們兩個差很少吧。

相關文章
相關標籤/搜索