Java併發編程3--認識Volatile和JMM


本文很多借鑑( Java併發編程的藝術 方騰飛 魏鵬 程曉明 著),讀好書,讀正版書。

1.初步認識 Volatile

一段代碼引發的思考

public class VolatileDemo {
	public static boolean stop = false;

	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(() -> {
			int i = 0;
			while (!stop) {
				i++;
			}
		});
		thread.start();
		System.out.println("begin start thread");
		Thread.sleep(1000);
		stop = true;
	}
}	

上面這段代碼,演示了一個使用 volatile 以及沒使用volatile 這個關鍵字,對於變量更新的影響。不使用volatile該循環不會結束,使用了volatile 則會跳出循環。

volatile 的作用

volatile 可以使得在多處理器環境下保證了共享變量的可見性(線程通信的兩種機制:共享內存、消息傳遞),在多線程環境下,讀和寫發生在
不同的線程中的時候,可能會出現:讀線程不能及時的讀取到其他線程寫入的最新的值。這就是所謂的可見性。爲了實現跨線程寫入的內存可見性,必須使用到一些機制來實現。而 volatile 就是這樣一種機制。如果一個字段被聲明成volatile,Java線程內存 模型確保所有線程看到這個變量的值是一致的。

volatile 關鍵字是如何保證可見性的?

在修改帶有 volatile 修飾的成員變量時,會多一個 lock 指令。lock是一種控制指令,在多處理器環境下,lock 彙編指令可以基於總線鎖或者緩存鎖的機制來達到可見性的一個效果。主要做了兩件事:

  1. 將當前處理器緩存行的數據寫回到系統內存。
  2. 這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。

2.JMM

什麼是 JMM

JMM 全稱是 Java Memory Model,java內存模型. 什麼是 JMM 呢?導致可見性問題的根本原因是緩存以及重排序。 而 JMM 實際上就是屏蔽了各種硬件和操作系統的訪問差異的,提供了合理的禁用緩存以及禁止重排序的方法。所以它最核心的價值在於解決可見性和有序性。

它定義了共享內存中多線程序讀寫操作的行爲規範: 在虛擬機中把共享變量存儲到內存以及從內存中取出共享變量的底層實現細節。 通過這些規則來規範對內存的讀寫操作從而保證指令的正確性,它解決了 CPU 多級緩存、處理器優化、指令重排序導致的內存訪問問題,保證了併發場景下的可見性。

看下圖(摘自 Java併發編程的藝術 方騰飛 魏鵬 程曉明 著),JMM 抽象模型分爲主內存、工作內存(本地內存);主內存是所有線程共享的,一般是實例對象、靜態字段、數組對象等存儲在堆內存中的變量。工作內存是每個線程獨佔的,線程對變量的所有操作都必須在工作內存中進行,不能直接讀寫主內存中的變量,線程之間的共享變量值的傳遞都是基於主內存來完成。

在這裏插入圖片描述
從圖中來看,如果線程A與線程B之間要通信的話,必須要經歷下面2個步驟。

  1. 線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 線程B到主內存中去讀取線程A之前已更新過的共享變量。

下面這張圖解釋了上面的兩個步驟(摘自 Java併發編程的藝術 方騰飛 魏鵬 程曉明 著):
在這裏插入圖片描述
本地內存A和本地內存B由主內存中共享變量x的副本。假設初始時,這3個 內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存 A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內 存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時 線程B的本地內存的x值也變爲了1。

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

重排序

爲什麼代碼會重排序?
在執行程序時,爲了提高性能,編譯器和處理器常常會對指令做重排序。但是不能隨意重排序,不是你想怎麼排序就怎麼排序,它需要滿足以下兩個條件:

  • 在單線程環境下不能改變程序運行的結果。
  • 存在數據依賴關係的不允許重排序

重排序分3種類型

  1. 編譯器優化的重排序。 編譯器在不改變單線程程序語義的前提下,可以重新安排語句 的執行順序。
  2. 指令級並行的重排序。 現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應 機器指令的執行順序。
  3. 內存系統的重排序。 由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上 去可能是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序:
在這裏插入圖片描述
上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序可能會導致多線程程序 出現內存可見性問題。

  • 對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排 序(不是所有的編譯器重排序都要禁止)。
  • 對於處理器重排序,JMM的處理器重排序規則會要 求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之爲 Memory Fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序。

JMM 層面的內存屏障

爲了保證內存可見性,Java 編譯器在生成指令序列的適當位置會插入內存屏障來禁止特定類型的處理器的重排序,在 JMM 中把內存屏障分爲四類:
在這裏插入圖片描述
當基於共享可變狀態的內存操作被重新排序時,程序可能行爲不定。一個線程寫入的數據可能被其他線程可見,原因是數據寫入的順序不一致。適當的放置內存屏障,通過強制處理器順序執行待定的內存操作來避免這個問題。

HappenBefore

它的意思表示的是前一個操作的結果對於後續操作是可見的,所以它是一種表達多個線程之間對於內存的可見性。所以我們可以認爲在 JMM 中,如果一個操作執行的結果需要對另一個操作可見,那麼這個操作必須要存在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。

happens-before與JMM的關係:

在這裏插入圖片描述
注:兩個操作之間存在happens-before關係,並不意味着一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。很早之前寫的介紹happens-before

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