blp模型 上读下写_Java高并发编程(三):Java内存模型
1 Java內存模型的基礎
在并發編程里,需要處理兩個問題:
通信指的是線程之間以何種機制來交換信息。在命令式編程里中,線程之間的通信機制有兩種:共享內存和消息傳遞。 Java的并發采用的是共享內存模型。
1.1 Java內存模型的抽象結構
Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀、寫共享變量的副本。
從圖中可以看到,如果線程A和線程B之間要通信的話,必須經歷如下的2步:
如圖,假設初始時,本地內存A、B以及主內存中X均為0,線程A在執行時,把更新后的x值(假設為1)臨時存放在自己的本地內存A中。當線程A和現場B需要通信時,線程A首先會把自己的本次內存中修改的x值刷新到主內存中,此時主內存中的x值變成了1。隨后線程B到主內存中去讀取線程A更新后的x值,此時線程B的本地內存中的x值也變成了1。
1.2 從源代碼到指令序列的重排序
我們了解了Java內存模型的抽象結構之后,下面我們來簡單聊一下一段Java代碼到編譯成字節碼之后,再到最后處理器運行時進行指令序列的重排序過程。
在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分為以下3種:
從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序,如下圖:
1.3 并發編程模型的分類
為了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障指令分為4類:
1.4 happens-before簡介
如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這兩個操作可以在同一個線程中,也可以在不同的線程中。
2 指令重排序
重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
2.1 數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。
數據依賴性分為以下三種:
- 寫后讀
- 寫后寫
- 讀后寫
這里所說的數據依賴性僅針對于單個處理器中執行的指令序列和單個線程中執行的操作
2.2 as-if-serial語義
as-if-serial的意思是,不管怎么重排序,(單線程)程序的執行結果不能被改變。 所以為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為重排序會改變執行的結果。反之,如果不存在數據依賴關系,這些操作是可以被編譯器和處理器重排序的。
2.3 程序順序規則
在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,盡可能提高并行度。
3. 順序一致性
順序一致性模型是一個被計算機科學家理想化了的理想參考模型。
3.1 數據競爭與順序一致性
JMM對正確同步的多線程程序的內存一致性做了如下的保證:
順序一致性定義:如果程序是正確同步的,程序的執行將具有順序一致性(Sequentially Consistent)—— 即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。
3.2 順序一致性內存模型
順序一致性模型具有以下兩大特性:
在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程上,同時每一個線程都必須按照程序的順序來執行內存讀/內存寫操作。
3.3 未同步程序的執行特性
JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。未同步程序在JMM內存模型和順序一致性模型中存在以下幾個差異:
在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱為總線事務(Bus Transaction)。總線事務包括讀事務(Read Transaction)和寫事務(Write Transaction)。總線處理具有總線鎖定來同步對總線事務操作,在處理器執行總線事務期間,總線會禁止其他的處理器和I/O設備執行內存的讀/寫操作。
總線的這些工作機制可以把所有處理器對內存的訪問以串行化的方式來執行。在任意時間,最多只有一個處理器可以訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性。
那么為什么JMM不保證對64位的long性和double型變量的寫操作具有原子性?
在一些32位的處理器上,如果要求對64位的數據寫操作具有原子性,會有較大的開銷。當JMM在這種處理器上運行時,可能會把一個64位long/double類型的變量的寫操作拆分為兩個32位的寫操作進行執行。這兩個32位的寫操作可能會被分配到不同的總線事務中執行,此時對這兩個64位變量的寫操作不具有原子性。4. volatile的內存語義
4.1 volatile的特性
理解volatile特性的一個好方法是對volatile變量的單個讀、寫,看成是使用同一個鎖對這些單個讀、寫操作做了同步。
簡而言之,volatile變量自身具有如下特性:
- 可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量的最后的寫入
- 對任意單個volatile變量的讀、寫具有原子性。但是對于類似于volatile++的復合操作不具備原子性。
4.2 volatile寫-讀建立的happens-before關系
volatile變量規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀。
如下代碼:
class VolatileExample(){int a = 0;volatile boolean flag = false;public void writer(){a = 1; // 1flag = true; // 2}public void reader(){if(flag){ // 3int i = a ; // 4 .....}} }假設線程A執行writer()方法之后,線程B執行reader()方法,其happens-before關系的圖形化表現形式如下:
4.3 volatile寫-讀的內存語義
volatile的內存語義的總結:
4.4 volatile 內存語義的實現
volatile 內存語義通過使用store、write和read、load原子操作指令以及內存屏障來實現。
下面是基于保守策略的JMM內存屏障插入策略:
volatile關鍵字與鎖的同步策略的優勢和劣勢:
由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖的互斥執行的特性可以確保對整個臨界區代碼的執行具有原子性。在功能上,鎖比volatile更強大;在可伸縮性和執行性能上,volatile更有優勢。5. 鎖的內存語義
5.1 鎖的釋放–獲取建立的happens-before關系
鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發送消息。
5.2 鎖的釋放后和獲取的內存語義
總結:
鎖(synchronized重量級鎖)內存語義的實現:
synchronized重量級鎖主要是通過lock(鎖定)、unlock(解鎖)原子指令來實現的。lock(鎖定)、unlock(解鎖)有兩個規則:- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許unlock一個被其他線程鎖定的變量。
- 對一個變量執行unlock操作,必須把此變量同步到主內存中(執行store和write操作)
5.3 鎖(ReentrantLock)內存語義的實現
下面我們來看以下代碼:
class ReentrantLockExample{int a = 0;ReentrantLock lock = new ReentrantLock();public void writer(){lock.lock(); //獲取鎖try {a++;} finally {lock.unlock(); //釋放鎖}} public void reader(){lock.lock(); //獲取鎖try {int i = a;.....} finally {lock.unlock(); //釋放鎖}} }ReentrantLock的實現依賴于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一個整型的volatile變量來維護同步狀態。
ReentrantLock分為公平鎖和非公平鎖。ReentrantLock默認是非公平鎖
5.3.1 非公平鎖
使用非公平鎖時,加鎖方法lock()調用軌跡如下。 1)ReentrantLock:lock()。 2)NonfairSync:lock()。 3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。 在第3步真正開始加鎖,下面是該方法的源代碼。
protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }該方法以原子操作的方式更新state變量,本文把Java的compareAndSet()方法調用簡稱為 CAS。JDK文檔對該方法的說明如下:如果當前狀態值等于預期值,則以原子方式將同步狀態 設置為給定的更新值。此操作具有volatile讀和寫的內存語義。
5.3.2 公平鎖
使用公平鎖時,加鎖方法lock()調用軌跡如下。
- 1)ReentrantLock:lock()。
- 2)FairSync:lock()。
- 3)AbstractQueuedSynchronizer:acquire(int arg)。
- 4)ReentrantLock:tryAcquire(int acquires)。
在第4步真正開始加鎖,下面是該方法的源代碼。
/*** Fair version of tryAcquire. Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}從上面源代碼中我們可以看出,加鎖方法首先讀volatile變量state。
在使用公平鎖時,解鎖方法unlock()調用軌跡如下。
- 1)ReentrantLock:unlock()。
- 2)AbstractQueuedSynchronizer:release(int arg)。
- 3)Sync:tryRelease(int releases)。
在第3步真正開始釋放鎖,下面是該方法的源代碼。
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}從上面的源代碼可以看出,在釋放鎖的最后寫volatile變量state。
公平鎖在釋放鎖的最后寫volatile變量state,在獲取鎖時首先讀這個volatile變量。根據 volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量后將立即變得對獲取鎖的線程可見。
現在對公平鎖和非公平鎖的內存語義做個總結:
5.4 concurrent包的實現
Java的CAS會使用現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子 方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。
如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴于這些基礎類來實現的。
6. final域的內存語義
6.1 final域的重排序規則
示例代碼如下:
public clas FinalExample{int i ; // 普通變量final int j; // final變量static FinalExample obj;public FinalExample(){ // 構造函數i = 1; // 寫普通域j = 2; // 寫final域}public static void writer(){ // 寫線程A執行obj = new FinalExample();}public static void reader(){ // 讀線程B執行FinalExample object = obj; // 讀對象引用int a = object.i; // 讀普通域int b = object.j; // 讀final域} }假設一個線程A執行writer()方法,隨后另一個線程B執行reader()方法。
6.2 寫final域的重排序規則
6.3 讀final域的重排序規則
在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。
6.4 final域為引用類型
在構造函數內對一個final引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
讀者可能會問:為什么final引用不能從構造函數內“溢出”?
在構造函數返回前,被構造對象的引用不能為其他線程所見,因為此時final域可能還沒有被初始化。在構造函數返回后,任意線程都將保證能看到final域正確初始化之后的值。7. happens-before 先行發生原則
7.1 JMM的設計
設計意圖:
JMM對這兩種不同性質的重排序,采取了不同的策略,如下。
7.2 happens-before定義
JSR-133使用happens-before的概念來指定兩個操作之間的執行順序。先行發生是Java內存模型中定義的兩項操作之間的偏序關系。如果說操作A先行發生于操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到。
7.3 happens-before原則
- 1)程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作。
- 2)監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
- 3)volatile變量規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀。
- 4)傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- 5)start()規則:如果線程A執行操作ThreadB.start()(啟動線程B),那么A線程的 ThreadB.start()操作happens-before于線程B中的任意操作。
- 6)join()規則:如果線程A執行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
7.4 雙重檢查鎖定與延遲初始化
雙重檢查鎖定與延遲初始化在Java多線程程序中,有時候需要采用延遲初始化來降低初始化類和創建對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法.
7.4.1雙重檢查鎖定的由來
假設在Java程序程序中,我們需要使用單例設計模式。下面是一個簡單的單例設計模式:
public class UnsafeLazyInitialization {private static Instance instance;public static Instance getInstance() {if (instance == null) // 1:A線程執行instance = new Instance(); // 2:B線程執行return instance;} }問題是:上面代碼描述的單例模式不是線程安全的。如果有多個線程同時訪問時,訪問結果是線程不安全的。所以我們只需要添加同步鎖synchronized關鍵字即可
public class SafeLazyInitialization {private static Instance instance;public synchronized static Instance getInstance() {if (instance == null)instance = new Instance();return instance;}}問題:由于對getInstance()方法做了同步處理,synchronized將導致性能開銷。如果getInstance()方法被多個線程頻繁的調用,將會導致程序執行性能的下降。為了解決這個問題,人們就提出了使用雙重檢查鎖定來實現延遲初始化的示例代碼。
public class DoubleCheckedLocking { // 1private static Instance instance; // 2public static Instance getInstance() { // 3if (instance == null) { // 4:第一次檢查synchronized (DoubleCheckedLocking.class) { // 5:加鎖if (instance == null) // 6:第二次檢查instance = new Instance(); // 7:問題的根源出在這里} // 8} // 9return instance; // 10} // 11 }如上面代碼所示,如果第一次檢查instance不為null,那么就不需要執行下面的加鎖和初始 化操作。因此,可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來,似乎兩全其美。但是它是錯誤的,上面代碼在執行的過程中,也會造成線程不安全的問題。
7.4.2 出現問題的原因
簡單來說,出現問題的元素就是指令重排序的問題。
前面的雙重檢查鎖定示例代碼的第7行(instance=new Singleton();)創建了一個對象。這一行代碼可以分解為如下的3行偽代碼。
memory = allocate(); // 1:分配對象的內存空間 ctorInstance(memory); // 2:初始化對象 instance = memory; // 3:設置instance指向剛分配的內存地址上面代碼可能會進行重排序:
memory = allocate(); // 1:分配對象的內存空間 instance = memory; // 3:設置instance指向剛分配的內存地址 // 注意,此時對象還沒有被初始化! ctorInstance(memory); // 2:初始化對象在知曉了問題發生的根源之后,我們可以想出兩個辦法來實現線程安全的延遲初始化。
7.4.3 基于volatile的解決方案
volatile關鍵字禁止指令重排序。我們可以利用這個特性,使用volatile關鍵字來解決這個問題:
public class SafeDoubleCheckedLocking {private volatile static Instance instance;public static Instance getInstance() {if (instance == null) {synchronized (SafeDoubleCheckedLocking.class) {if (instance == null)instance = new Instance(); // instance為volatile,現在沒問題了}}return instance;} }7.4.4 基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會執行類的初始化。在 執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
public class InstanceFactory {private static class InstanceHolder {public static Instance instance = new Instance();}public static Instance getInstance() {return InstanceHolder.instance ; // 這里將導致InstanceHolder類被初始化} }這個方案的實質是:允許3.8.2節中的3行偽代碼中的2和3重排序,但不允許非構造線程(這 里指線程B)“看到”這個重排序。
Java虛擬機類加載的條件:
- 1)T是一個類,而且一個T類型的實例被創建。
- 2)T是一個類,且T中聲明的一個靜態方法被調用。
- 3)T中聲明的一個靜態字段被賦值。
- 4)T中聲明的一個靜態字段被使用,而且這個字段不是一個常量字段。
- 5)T是一個頂級類(Top Level Class,見Java語言規范的§7.6),而且一個斷言語句嵌套在T內部被執行。
Java語言規范規定,對于每一個類或接口C,都有一個唯一的初始化鎖LC與之對應。從C 到LC的映射,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,并且 每個線程至少獲取一次鎖來確保這個類已經被初始化過了.
7.4.5 多線程類加載處理過程
第1階段
第一階段:通過在Class對象上同步(即獲取Class對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程能夠獲取到這個初始化鎖。
第二階段
第2階段:線程A執行類的初始化,同時線程B在初始化鎖對應的condition上等待。
第三階段
第3階段:線程A設置state=initialized,然后喚醒在condition中等待的所有線程。
第四階段
第4階段:線程B結束類的初始化處理。
第五階段
第5階段:線程C執行類的初始化的處理。
8.Java內存模型綜述
8.1 處理器的內存模型
處理器的內存模型順序一致性內存模型是一個理論參考模型,JMM和處理器內存模型在設計時通常會以順序一致性內存模型為參照。
9.2 各種內存模型之間的關系
JMM是一個語言級的內存模型,處理器內存模型是硬件級的內存模型,順序一致性內存 模型是一個理論參考模型。
9.3 JMM的內存可見性保證
按程序類型,Java程序的內存可見性保證可以分為下列3類。
總結
以上是生活随笔為你收集整理的blp模型 上读下写_Java高并发编程(三):Java内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: expdp oracle 并行_关于Ex
- 下一篇: asp.net httpclient p