Java volatile關鍵字最全總結:原理剖析與例項講解(簡單易懂)

老鼠只愛大米發表於2018-06-13

目錄

一、簡介

二、併發程式設計的3個基本概念

三、鎖的互斥和可見性

四、Java的記憶體模型JMM以及共享變數的可見性

五、volatile變數的特性

六、volatile不適用的場景

七、volatile原理

八、單例模式的雙重鎖為什麼要加volatile


一、簡介

volatile是Java提供的一種輕量級的同步機制。Java 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變數,相比於synchronized(synchronized通常稱為重量級鎖),volatile更輕量級,因為它不會引起執行緒上下文的切換和排程。但是volatile 變數的同步性較差(有時它更簡單並且開銷更低),而且其使用也更容易出錯。

二、併發程式設計的3個基本概念

(1)原子性

定義: 即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行

原子性是拒絕多執行緒操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個執行緒來對它進行操作。簡而言之,在整個操作過程中不會被執行緒排程器中斷的操作,都可認為是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

a. 基本型別的讀取和賦值操作,且賦值必須是數字賦值給變數,變數之間的相互賦值不是原子性操作。

b.所有引用reference的賦值操作

c.java.concurrent.Atomic.* 包中所有類的一切操作

(2)可見性

定義:指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

在多執行緒環境下,一個執行緒對共享變數的操作對其他執行緒是不可見的。Java提供了volatile來保證可見性,當一個變數被volatile修飾後,表示著執行緒本地記憶體無效,當一個執行緒修改共享變數後他會立即被更新到主記憶體中,其他執行緒讀取共享變數時,會直接從主記憶體中讀取。當然,synchronize和Lock都可以保證可見性。synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。

(3)有序性

定義:即程式執行的順序按照程式碼的先後順序執行。

Java記憶體模型中的有序性可以總結為:如果在本執行緒內觀察,所有操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句是指“執行緒內表現為序列語義”,後半句是指“指令重排序”現象和“工作記憶體主主記憶體同步延遲”現象。

在Java記憶體模型中,為了效率是允許編譯器和處理器對指令進行重排序,當然重排序不會影響單執行緒的執行結果,但是對多執行緒會有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式裡面的DCL(雙重檢查鎖)。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼,自然就保證了有序性。

三、鎖的互斥和可見性

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。

(1)互斥即一次只允許一個執行緒持有某個特定的鎖,一次就只有一個執行緒能夠使用該共享資料。

(2)可見性要更加複雜一些,它必須確保釋放鎖之前對共享資料做出的更改對於隨後獲得該鎖的另一個執行緒是可見的。也即當一條執行緒修改了共享變數的值,新值對於其他執行緒來說是可以立即得知的。如果沒有同步機制提供的這種可見性保證,執行緒看到的共享變數可能是修改前的值或不一致的值,這將引發許多嚴重問題。要使 volatile 變數提供理想的執行緒安全,必須同時滿足下面兩個條件:

a.對變數的寫操作不依賴於當前值。

b.該變數沒有包含在具有其他變數的不變式中。

實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立於任何程式的狀態,包括變數的當前狀態。事實上就是保證操作是原子性操作,才能保證使用volatile關鍵字的程式在併發時能夠正確執行。

四、Java的記憶體模型JMM以及共享變數的可見性

JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見,JMM定義了執行緒和主記憶體之間的抽象關係:共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體儲存了被該執行緒使用到的主記憶體的副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。

對於普通的共享變數來講,執行緒A將其修改為某個值發生線上程A的本地記憶體中,此時還未同步到主記憶體中去;而執行緒B已經快取了該變數的舊值,所以就導致了共享變數值的不一致。解決這種共享變數在多執行緒模型中的不可見性問題,較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,比較合理的方式其實就是volatile。

需要注意的是,JMM是個抽象的記憶體模型,所以所謂的本地記憶體,主記憶體都是抽象概念,並不一定就真實的對應cpu快取和實體記憶體

五、volatile變數的特性

(1)保證可見性,不保證原子性

a.當寫一個volatile變數時,JMM會把該執行緒本地記憶體中的變數強制重新整理到主記憶體中去;

b.這個寫會操作會導致其他執行緒中的快取無效。

(2)禁止指令重排

重排序是指編譯器和處理器為了優化程式效能而對指令序列進行排序的一種手段。重排序需要遵守一定規則:

  a.重排序操作不會對存在資料依賴關係的操作進行重排序。

  比如:a=1;b=a; 這個指令序列,由於第二個操作依賴於第一個操作,所以在編譯時和處理器運

行時這兩個操作不會被重排序。

b.重排序是為了優化效能,但是不管怎麼重排序,單執行緒下程式的執行結果不能被改變

  比如:a=1;b=2;c=a+b這三個操作,第一步(a=1)和第二步(b=2)由於不存在資料依賴關係, 所以可能會發

生重排序,但是c=a+b這個操作是不會被重排序的,因為需要保證最終的結果一定是c=a+b=3。

重排序在單執行緒下一定能保證結果的正確性,但是在多執行緒環境下,可能發生重排序,影響結果,下例中的1和2由於不存在資料依賴關係,則有可能會被重排序,先執行status=true再執行a=2。而此時執行緒B會順利到達4處,而執行緒A中a=2這個操作還未被執行,所以b=a+1的結果也有可能依然等於2。

使用volatile關鍵字修飾共享變數便可以禁止這種重排序。若用volatile修飾共享變數,在編譯時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序,volatile禁止指令重排序也有一些規則:

a.當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後

面的操作可見;在其後面的操作肯定還沒有進行;

b.在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放

到其前面執行。

即執行到volatile變數時,其前面的所有語句都執行完,後面所有語句都未執行。且前面語句的結果對volatile變

量及其後面語句可見。

六、volatile不適用的場景

(1)volatile不適合複合操作

例如,inc++不是一個原子性操作,可以由讀取、加、賦值3步組成,所以結果並不能達到30000。.

(2)解決方法

1.採用synchronized

2.採用Lock

3.採用java併發包中的原子操作類,原子操作類是通過CAS迴圈的方式來保證其原子性的

七、volatile原理

volatile可以保證執行緒可見性且提供了一定的有序性,但是無法保證原子性。在JVM底層volatile是採用“記憶體屏障”來實現的。觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令,lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

I. 它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到內

存屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

II. 它會強制將對快取的修改操作立即寫入主存;

III. 如果是寫操作,它會導致其他CPU中對應的快取行無效。

八、單例模式的雙重鎖為什麼要加volatile

需要volatile關鍵字的原因是,在併發情況下,如果沒有volatile關鍵字,在第5行會出現問題。instance = new TestInstance();可以分解為3行虛擬碼

a.memory = allocate() //分配記憶體

b. ctorInstanc(memory) //初始化物件

c. instance = memory //設定instance指向剛分配的地址

上面的程式碼在編譯執行時,可能會出現重排序從a-b-c排序為a-c-b。在多執行緒的情況下會出現以下問題。當執行緒A在執行第5行程式碼時,B執行緒進來執行到第2行程式碼。假設此時A執行的過程中發生了指令重排序,即先執行了a和c,沒有執行b。那麼由於A執行緒執行了c導致instance指向了一段地址,所以B執行緒判斷instance不為null,會直接跳到第6行並返回一個未初始化的物件。

相關文章