java編程,如何徹底理解volatile關鍵字?

黃元中


從幾道相關面試題說起吧,volatile 常被這麼問:

談談你對 volatile 的理解?

你知道 volatile 底層的實現機制嗎?

volatile 變量和 atomic 變量有什麼不同?

volatile 的使用場景,你能舉兩個例子嗎?

我們知道Java 內存模型——JMM, JMM是圍繞著併發過程中如何處理可見性、原子性和有序性這 3 個 特徵建立起來的,而 volatile 可以保證其中的兩個特性,下面具體探討下這個面試必問的關鍵字。

1. 概念

volatile 是 Java 中的關鍵字,是一個變量修飾符,用來修飾會被不同線程訪問和修改的變量。

2. Java 內存模型 3 個特性

2.1 可見性

可見性是一種複雜的屬性,因為可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。

可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。

在 Java 中 volatile、synchronized 和 final 都可以實現可見性。


2.2 原子性

原子性指的是某個線程正在執行某個操作時,中間不可以被加塞或分割,要麼整體成功,要麼整體失敗。比如 a=0;(a非long和double類型) 這個操作是不可分割的,那麼我們說這個操作是原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。Java的 concurrent 包下提供了一些原子類,AtomicInteger、AtomicLong、AtomicReference等。

在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。


2.3 有序性

Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。


3. volatile 是 Java 虛擬機提供的輕量級的同步機制

  • 保證可見性

  • 不保證原子性

  • 禁止指令重排(保證有序性)


3.1 空說無憑,代碼驗證

3.1.1 可見性驗證

class MyData {

int number = 0;

public void add() {

this.number = number + 1;

}

}

// 啟動兩個線程,一個work線程,一個main線程,work線程修改number值後,查看main線程的number

private static void testVolatile() {

MyData myData = new MyData();

new Thread(() -> {

System.out.println(Thread.currentThread().getName()+"\\t come in");

try {

TimeUnit.SECONDS.sleep(2);

myData.add();

System.out.println(Thread.currentThread().getName()+"\\t update number value :"+myData.number);

} catch (InterruptedException e) {

e.printStackTrace();

}

}, "workThread").start();

//第2個線程,main線程

while (myData.number == 0){

//main線程還在找0

}

System.out.println(Thread.currentThread().getName()+"\\t mission is over");

System.out.println(Thread.currentThread().getName()+"\\t mission is over,main get number is:"+myData.number);

}

}

運行 方法,輸出如下,會發現在 main 線程死循環,說明 main 線程的值一直是 0

workThread\t execute

workThread\t update number value :1

修改 ,,在 number 前加關鍵字 volatile,重新運行,main 線程獲取結果為 1

workThread\t execute

workThread\t update number value :1

main\t execute over,main get number is:1


3.1.2 不保證原子性驗證

class MyData {

volatile int number = 0;

public void add() {

this.number = number + 1;

}

}

private static void testAtomic() throws InterruptedException {

MyData myData = new MyData();

for (int i = 0; i < 10; i++) {

new Thread(() ->{

for (int j = 0; j < 1000; j++) {

myData.addPlusPlus();

}

},"addPlusThread:"+ i).start();

}

//等待上邊20個線程結束後(預計5秒肯定結束了),在main線程中獲取最後的number

TimeUnit.SECONDS.sleep(5);

while (Thread.activeCount() > 2){

Thread.yield();

}

System.out.println("final value:"+myData.number);

}

運行 發現最後的輸出值,並不一定是期望的值 10000,往往是比 10000 小的數值。

final value:9856


為什麼會這樣呢,因為 在轉化為字節碼指令的時候是4條指令

  • 獲取原始值

  • 將值入棧

  • 進行加 1 操作

  • 把 後的操作寫回主內存

這樣在運行時候就會存在多線程競爭問題,可能會出現了丟失寫值的情況。

如何解決原子性問題呢?

加 或者直接使用 原子類。


3.1.3 禁止指令重排驗證

計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分為以下 3 種

處理器在進行重排序時必須要考慮指令之間的

數據依賴性,我們叫做 語義

單線程環境裡確保程序最終執行結果和代碼順序執行的結果一致;但是多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。

我們往往用下面的代碼驗證 volatile 禁止指令重排,如果多線程環境下,`最後的輸出結果不一定是我們想象到的 2,這時就要把兩個變量都設置為 volatile。

public class ReSortSeqDemo {

int a = 0;

boolean flag = false;

public void mehtod1(){

a = 1;

flag = true;

}

public void method2(){

if(flag){

a = a +1;

System.out.println("reorder value: "+a);

}

}

}

實現禁止指令重排優化,從而避免了多線程環境下程序出現亂序執行的現象。


還有一個我們最常見的多線程環境中 版本的單例模式中,就是使用了 volatile 禁止指令重排的特性。

public class Singleton {

private static volatile Singleton instance;

private Singleton(){}

// DCL

public static Singleton getInstance(){

if(instance ==null){ //第一次檢查

synchronized (Singleton.class){

if(instance == null){ //第二次檢查

instance = new Singleton();

}

}

}

return instance;

}

}

因為有指令重排序的存在,雙端檢索機制也不一定是線程安全的。

why ?

Because: 初始化對象的過程其實並不是一個原子的操作,它會分為三部分執行,

  1. 給 instance 分配內存
  2. 調用 instance 的構造函數來初始化對象
  3. 將 instance 對象指向分配的內存空間(執行完這步 instance 就為非 null 了)

步驟 2 和 3 不存在數據依賴關係,如果虛擬機存在指令重排序優化,則步驟 2和 3 的順序是無法確定的。如果A線程率先進入同步代碼塊並先執行了 3 而沒有執行 2,此時因為 instance 已經非 null。這時候線程 B 在第一次檢查的時候,會發現 instance 已經是 非null 了,就將其返回使用,但是此時 instance 實際上還未初始化,自然就會出錯。所以我們要限制實例對象的指令重排,用 volatile 修飾(JDK 5 之前使用了 volatile 的雙檢鎖是有問題的)。


4. 原理

volatile 可以保證線程可見性且提供了一定的有序性,但是無法保證原子性。在 JVM 底層是基於內存屏障實現的。

  • 當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到 CPU 緩存中。如果計算機有多個CPU,每個線程可能在不同的 CPU 上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中
  • 而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步,所以就不會有可見性問題
    • 對 volatile 變量進行寫操作時,會在寫操作後加一條 store 屏障指令,將工作內存中的共享變量刷新回主內存;
    • 對 volatile 變量進行讀操作時,會在寫操作後加一條 load 屏障指令,從主內存中讀取共享變量;

通過 hsdis 工具獲取 JIT 編譯器生成的彙編指令來看看對 volatile 進行寫操作CPU會做什麼事情,還是用上邊的單例模式,可以看到

PS:具體的彙編指令對我這個 Javaer 太南了,但是 JVM 字節碼我們可以認識, 的含義是給一個靜態變量設置值,那這裡的 ,而且是第 17 行代碼,更加確定是給 instance 賦值了。果然像各種資料裡說的,找到了 據說還得翻閱。這裡可以看下這兩篇 https://www.jianshu.com/p/6ab7c3db13c3 、 https://www.cnblogs.com/xrq730/p/7048693.html )

有 volatile 修飾的共享變量進行寫操作時會多出第二行彙編代碼,該句代碼的意思是對原值加零,其中相加指令addl前有 lock 修飾。通過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引發兩件事情:

  • 將當前處理器緩存行的數據寫回到系統內存

  • 這個寫回內存的操作會引起在其他CPU裡緩存了該內存地址的數據無效

正是 lock 實現了 volatile 的「防止指令重排」「內存可見」的特性


5. 使用場景

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴於當前值

  • 該變量沒有包含在具有其他變量的不變式中

其實就是在需要保證原子性的場景,不要使用 volatile。

6. volatile 性能

volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。

引用《正確使用 volaitle 變量》一文中的話:

很難做出準確、全面的評價,例如 “X 總是比 Y 快”,尤其是對 JVM 內在的操作而言。(例如,某些情況下 JVM 也許能夠完全刪除鎖機制,這使得我們難以抽象地比較 和 的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現內存界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低。

volatile 操作不會像鎖一樣造成阻塞,因此,在能夠安全使用 volatile 的情況下,volatile 可以提供一些優於鎖的可伸縮特性。如果讀操作的次數要遠遠超過寫操作,與鎖相比,volatile 變量通常能夠減少同步的性能開銷。


參考

《深入理解Java虛擬機》

http://tutorials.jenkov.com/java-concurrency/java-memory-model.html https://juejin.im/post/5dbfa0aa51882538ce1a4ebc

《正確使用 Volatile 變量》

https://www.ibm.com/developerworks/cn/java/j-jtp06197.htm

l

JavaKeeper



JMM 基礎-計算機原理


Java 內存模型即Java Memory Model,簡稱JMM,JMM定義了Java 虛擬機(JVM)在計算機(RAM) 中的工作方式。 JVM 是整個計算機虛擬模型,所以JMM是隸屬於JVM的,



在計算機系統中,寄存器是L0級緩存,接著依次是L1,L2,L3(接下來是內存,本地磁盤,遠程存儲).越往上的緩存存儲空間越小,速度越快,成本也越高。越往下的存儲空間越大,速度更慢,成本也越低。


從上至下,每一層都都可以是看作是更下一層的緩存,即:L0寄存器是L1一級緩存的緩存,

L1是L2的緩存,一次類推;每一層的數據都是來至於它的下一層。


在現在CPU上,一般來說L0,L1,L2,L3都繼承在CPU內部,而L1還分為一級數據緩存和一級指令緩存,分別用於存放數據和執行數據的指令解碼,每個核心擁有獨立的運算處理單元、控制器、寄存器、L1緩存、L2 緩存,然後一個CPU的多個核心共享最後一層CPU緩存L3。


CPU 的緩存一致性解決方案


分為以下兩種方案


  1. 總線鎖(每次鎖總線,是悲觀鎖)

  2. 緩存鎖(只鎖緩存的數據)




MESI協議如下:

  • M(modify):
  • I(invalid)
  • E(Exclusive)
  • S(Share)


JMM內存模型的八種同步操作


1、read(讀取),從主內存讀取數據


2、load(載入):將主內存讀取到的數據寫入到工作內存


3、use(使用): 從工作內存讀取數據來計算


4、assign(賦值):將計算好的值重新賦值到工作內存中


5、store(存儲):將工作內存數據寫入主內存


6、write(寫入):將store過去的變量值賦值給主內存中的變量


7、lock(鎖定):將主內存變量加鎖,標識為線程 獨佔狀態


8、unlock(解鎖):將主內存變量解鎖,解鎖後其他線程可以鎖定該變量



Java 內存模型帶來的問題


1、可見性問題



左邊CPU中運行的線程從主內存中拷貝對象obj到它的CPU緩存,把對象obj的count變量改為2,但這個變更對運行在右邊的CPU中的線程是不可見,因為這個更改還沒有flush到主內存中。


在多線程環境下,如果某個線程首次讀取共享變量,則首先到主內存中獲取該變量,然後存入到工作內存中,以後只需要在工作內存中讀取該變量即可,同樣如果對該變量執行了修改的操作,則先將新值寫入工作內存中,然後再刷新至於內存中,但是什麼時候最新的值會被刷新到主內存中是不太確定的,一般來說是很快的,但是具體時間未知,,要解決共享對象可見性問題,我們可以使用volatile關鍵字或者加鎖。


2、競爭問題



線程A 和 線程B 共享一個對象obj, 假設線程A從主存讀取obj.count變量到自己的緩存中,同時,線程B也讀取了obj.count變量到它的CPU緩存,並且這兩個線程都對obj.count做了加1操作,此時,obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。


如果則兩個加1操作是串行執行的,那麼obj.count變量便會在原始值上加2,最終主內存中obj.count的值會為3,然後圖中兩個加1操作是並行的,不管是線程A還是線程B先flush計算結果到主存,最終主存中的obj.count只會增加1次變成2,儘管一共有兩次加1操作,要解決上面的問題我們可以使用synchronized 代碼塊。


3、重排序


除了共享內存和工作內存帶來的問題,還存在重排序的問題,在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。


重排序分3中類型:


(1) 編譯器優化的重排序。


(2) 指令級並行的重排序


(3)內存系統的重排序


① 數據依賴性


數據依賴性: 如果兩個操作訪問同一變量,且這兩個操作中有一個為寫,此時這兩個操作之間就存在數據依賴性。


依賴性分為以下三種:




上圖很明顯,A和C存在數據依賴,B和C也存在數據依賴,而A和B之間不存在數據依賴,如果重排序了A和C或者B和C的執行順序,程序的執行結果就會被改變。


很明顯,不管如何重排序,都必須保證代碼在單線程下的運行正確,連單線程下都無法保證,更不用討論多線程併發的情況,所以就提出一個as - if -serial 的概念。



4、as - if -serial


意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as - if -serial 語義。


A和C之間存在數據依賴,同時B和C之間也存在數據依賴關係,因此在最終執行的指令序列中,C不能被重排序A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關係,編譯器和處理器可以重排序A和B之間的執行順序。


as - if -serial 語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器可以讓我們感覺到: 單線程程序看起來是按程序的順序來執行的。as-if-srial語義使單線程程序無需擔心重排序干擾他們,也無需擔心內存可見性的問題。


5、內存屏障


Java 編譯器在生成指令序列的適當位置會插入內存屏障來禁止特定類型的處理去重排序,從而讓程序按我們預想的流程去執行。


① 保證特定操作的執行順序


② 影響某些數據(或者是某條指令的執行結果)的內存可見性


編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化性能。插入一條Memory Barrier 會告訴編譯器和CPU ,不管什麼指令都不能和這條Memory Barrier 指令重排序。


Memory Barrier 所做的另外一件事是強制刷出各種CPU cache, 如一個Write-Barrier(寫入屏障)將刷出所在的Barrier 之前寫入cache的數據,因此,任何CPU上的線程都能讀取到這些數據的最新版本。


JMM把內存屏障指令分為4類:



StoreLoad Barriers 是一個"全能型"的屏障,它同時具有其他3個屏障的效果,



volatile 關鍵字介紹


1、保證可見性


對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫。


我們先看下面代碼:


initFlag 沒有用volatile關鍵字修飾;


上面結果為:


說明一個線程改變initFlag狀態,另外一個線程看不見;


如果加上volatile關鍵字呢?


結果如下:


我們通過彙編看下代碼的最終底層實現:



volatile寫的內存語義如下:

當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。

當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。

比如:


如果我們將flag變量以volatile關鍵字修飾,那麼實際上:線程A在寫flag變量後,本地內存A中被線程A更新過的兩個共享變量的值都被刷新到主內存中。



在讀flag變量後,本地內存B包含的值已經被置為無效。此時,線程B必須從主內存中讀取共享變量。線程B的讀取操作將導致本地內存B與主內存中的共享變量的值變成一致。


如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀線程B讀一個volatile變量後,寫線程A在寫這個volatile變量之前所有可見的共享變量的值都將立即變得對讀線程B可見。


2、原子性

volatile 不保證變量的原子性;


運行結果如下:


因為count ++;


包含 三個操作:


(1) 讀取變量count

(2) 將count變量的值加1

(3) 將計算後的值再賦給變量count

從JMM內存分析:



下面從字節碼分析為什麼i++這種的用volatile修改不能保證原子性?


javap : 字節碼查看


其實i++這種操作主要可以分為3步:(彙編)

  1. 讀取volatile變量值到local
  2. 增加變量的值
  3. 把local的值寫回,讓其它的線程可見


Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在所有線程可見,也就是最後一步讓所有的CPU內核都獲得了最新的值,但

中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。




3、有序性


(1) volatile重排序規則表


① 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。


② 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。


③ 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。


(2) volatile的內存屏障


① volatile寫


storestore屏障:對於這樣的語句store1; storestore; store2,在store2及後續寫入操作執行前,保證store1的寫入操作對其它處理器可見。(也就是說如果出現storestore屏障,那麼store1指令一定會在store2之前執行,CPU不會store1與store2進行重排序)


storeload屏障:對於這樣的語句store1; storeload; load2,在load2及後續所有讀取操作執行前,保證store1的寫入對所有處理器可見。(也就是說如果出現storeload屏障,那麼store1指令一定會在load2之前執行,CPU不會對store1與load2進行重排序


② volatile讀



在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個loadstore屏障。


loadload屏障:對於這樣的語句load1; loadload; load2,在load2及後續讀取操作要讀取的數據被訪問前,保證load1要讀取的數據被讀取完畢。(也就是說,如果出現loadload屏障,那麼load1指令一定會在load2之前執行,CPU不會對load1與load2進行重排序)


loadstore屏障:對於這樣的語句load1; loadstore; store2,在store2及後續寫入操作被刷出前,保證load1要讀取的數據被讀取完畢。(也就是說,如果出現loadstore屏障,那麼load1指令一定會在store2之前執行,CPU不會對load1與store2進行重排序)


volatile的實現原理


volatile的實現原理

❶ 通過對OpenJDK中的unsafe.cpp源碼的分析,會發現被volatile關鍵字修飾的變量會存在一個“lock:”的前綴。


❷ Lock前綴,Lock不是一種內存屏障,但是它能完成類似內存屏障的功能。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖。


❸ 同時該指令會將當前處理器緩存行的數據直接寫會到系統內存中,且這個寫回內存的操作會使在其他CPU裡緩存了該地址的數據無效。


❹ 具體的執行上,它先對總線和緩存加鎖,然後執行後面的指令,最後釋放鎖後會把高速緩存中的髒數據全部刷新回主內存。在Lock鎖住總線的時候,其他CPU的讀寫請求都會被阻塞,直到鎖釋放。


【歡迎隨手關注@碼農的一天,希望對你有幫助】


碼農的一天


volatile在Java語言中扮演者重要的角色,它具有可見性以及禁止指令重排序兩個非常顯著的特點,要想解釋清楚volatile的用法,首先我們要對Java的內存模型JMM有一個非常熟悉的瞭解,所以我從以下幾點來分析volatile。

一、Java內存模型JMM

Java的內存模型規定:所有的變量都保存在主內存中,每一個線程都有屬於自己的工作內存,當讀取主內存的變量時,線程的工作內存都會都會存儲這個變量的副本,線程對變量的操作都是在自己的工作內存中,在適當的時候會把自己工作內存的變量同步到主內存中。

從上面的內容中可以得出一個結論,多線程對變量的修改,都是先修改自己的工作內存的變量,然後把工作內存中修改的在適當的時候同步到主內存中,那麼問題就來了,適當的時候是什麼時候呢?不確定,所以就有問題了,當主內存中有一個變量i=0,假如同時有兩個線程去修改i的值,當線程1讀取主內存中的i=1,然後拷貝一份副本在自己的工作內存中,然後i=1,但是這是操作的自己的工作內存i=1,但是這個i=1什麼時候刷新到主內存中呢?剛才我們說了,不確定,此時線程二讀取主存的變量i=0,然後也拷貝一份到自己的工作內存中,然後i=2,然後在適當的時候刷新到主存中,所以最終的結果可能是線程二i=2的結果先刷新到主存中,線程一i=1最後刷新到主存中,這就導致現在主存中i=1,所以與想象的結果不一樣。

二、volatile的大白話

瞭解了Java的內存模型JMM,我們瞭解了對於一個共享變量,如果有多個線程併發的修改這個共享變量,最終得到的結果可能與我們想象的不太一樣,這是由於JMM的機制導致的,而這和我們所說的volatile有什麼關係的,那接下來我們就說說。

結論:1:如果一個變量被volatile修飾,那麼它在工作內存中修改的變量會立刻被刷新到主存中。而不是上面所說的不確定的時候

2:如果讀取一個被volatile修飾的變量,會把此線程工作內存中的此變量副本置為無效,它會從主內存中重新讀取這個變量到自己的工作內存。

上面這兩點分別是volatile寫內存語義和volatile內存語義。

三、volatile在JDK的使用

在JDK中,併發包中volatile把它的特點發揮到了極致,尤其通過框架AQS的state就是被volatile修飾的,在加上CAS構建出了無鎖化的同步框架,在ConcurrentHashMap中也是因為有了volatile的作用加上CAS操作提高了很大的性能。

上面3點只是簡單的說明了volatile的作用,如果要詳細介紹volatile,估計能夠一本上百頁的書了,在這裡就不再詳述了,如果想進一步瞭解volatile,請關注我的頭條,我會有一個關於volatile的專題。


強哥教你學編程


volatile是Java虛擬機提供的輕量級的同步機制


  • 保證可見性
  • 不保證原子性
  • 禁止指令重排序


談談JVM




JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念 並不真實存在,


它描述的是一組規則或規範通過規範定製了程序中各個變量(包括實例字段,靜態字段和構成數組


對象的元素)的訪問方式.

JMM關於同步規定:

1.線程解鎖前,必須把共享變量的值刷新回主內存

2.線程加鎖前,必須讀取主內存的最新值到自己的工作內存

3.加鎖解鎖是同一把鎖

由於JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(有些地方成為棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作空間,然後對變量進行操作,操作完成再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存儲存著主內存中的變量副本拷貝,因此不同的線程無法訪問對方的工作內存,此案成間的通訊(傳值) 必須通過主內存來完成,其簡要訪問過程如下圖:


1、可見性


通過前面對JMM的介紹,我們知道各個線程對主內存中共享變量的操作都是各個線程各自拷貝到自己的工作內存操作後再寫回主內存中的.

這就可能存在一個線程AAA修改了共享變量X的值還未寫回主內存中時 ,另外一個線程BBB又對內存中的一個共享變量X進行操作,但此時A線程工作內存中的共享比那裡X對線程B來說並不不可見.這種工作內存與主內存同步延遲現象就造成了可見性問題.


2、原子性


為什麼 number++在多線程下是非線程安全的,如何不加synchronized解決?


3、有序性


計算機在執行程序時,為了提高性能,編譯器和處理器常常會做指令重排,一把分為以下3中

單線程環境裡面確保程序最終執行結果和代碼順序執行的結果一致.

處理器在進行重新排序是必須要考慮指令之間的數據依賴性

多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程使用的變量能否保持一致性是無法確定的,結果無法預測。


public void mySort(){

int x=11;//語句1

int y=12;//語句2

x=x+5;//語句3

y=x*x;//語句4

}

1234

2134

1324

問題:

請問語句4 可以重排後變成第一條碼?

存在數據的依賴性 沒辦法排到第一個


int a ,b ,x,y=0;

線程1\t線程2

x=a;\ty=b;

b=1;\ta=2;

x=0 y=0\t

如果編譯器對這段代碼進行執行重排優化後,可能出現下列情況:

線程1\t線程2

b=1;\ta=2;

x=a;\ty=b;

x=2 y=1\t

這也就說明在多線程環境下,由於編譯器優化重排的存在,兩個線程使用的變量能否保持一致是無法確定的.




你在哪些地方用到過volatile?


1、單例模式(雙重加鎖)


DCL(雙端檢鎖) 機制不一定線程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因在於某一個線程在執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化.

instance=new SingletonDem(); 可以分為以下步驟(偽代碼)

memory=allocate();//1.分配對象內存空間

instance(memory);//2.初始化對象

instance=memory;//3.設置instance的指向剛分配的內存地址,此時instance!=null

步驟2和步驟3不存在數據依賴關係.而且無論重排前還是重排後程序執行的結果在單線程中並沒有改變,因此這種重排優化是允許的.

memory=allocate();//1.分配對象內存空間

instance=memory;//3.設置instance的指向剛分配的內存地址,此時instance!=null 但對象還沒有初始化完.

instance(memory);//2.初始化對象

但是指令重排只會保證串行語義的執行一致性(單線程) 並不會關心多線程間的語義一致性

所以當一條線程訪問instance不為null時,由於instance實例未必完成初始化,也就造成了線程安全問題.



如果對你有幫助,歡迎關注@一直在coding


一直在coding


非java程序員,不過volatile在其他語言中也存在,簡單說下。

1,volatile只在多線程程序中有意義。

2,為了提高性能,編譯器工作時會進行一些優化,如指令排序,甚至跳過一些指令。如:

var a=1;

a=2;

a=3;

編譯後的結果可能就只執行 a = 3

3,程序運行時,普通變量會有緩存機制(如cpu緩存、線程本地緩存等),程序讀取時先從緩存讀取,所以多線程的程序運行時可能存在髒讀問題。即第一個線程已經修改了變量值,但第二個線程還在使用緩存中的舊數據。

volatile的作用就是告訴編譯器,不要對使用該變量的代碼進行優化,每次讀寫操作都訪問變量的原始數據。


熙爸愛釣魚


通常程序不會直接去操作CPU內核線程,而是通過內核線程的接口輕量級進程(LWP)來操作的,也就是通常意義上的線程.

系統在執行多線程任務時,數據存儲在RAM中,然而每個線程都有一個本地緩存,也就是CPU緩存,並不會每次都從RAM讀取數據,所以就會出現線程不安全的情況。

Java中volatile關鍵字主要是用來修飾變量使其能夠被線程可見.


飛昇的碼農


建議先學習java內存模型以及指令重排序,如此就能真正理解


俺餓了就怕


https://m.toutiaocdn.com/group/6682538998579069453/?app=news_article×tamp=1556016557&req_id=201904231849160100160441948694FA8&group_id=6682538998579069453

參考這篇文章


分享到:


相關文章: