Java中volatile關鍵字

1、簡介java

volatile是Java提供的一種輕量級的同步機制。Java 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量,相比於synchronized(synchronized一般稱爲重量級鎖),volatile更輕量級,由於它不會引發線程上下文的切換和調度。可是volatile 變量的同步性較差(有時它更簡單而且開銷更低),並且其使用也更容易出錯。c++

2、併發編程的3個基本概念
(1)原子性編程

定義: 即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。緩存

原子性是拒絕多線程操做的,不管是多核仍是單核,具備原子性的量,同一時刻只能有一個線程來對它進行操做。簡而言之,在整個操做過程當中不會被線程調度器中斷的操做,均可認爲是原子性。例如 a=1是原子性操做,可是a++和a +=1就不是原子性操做。Java中的原子性操做包括:安全

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

b.全部引用reference的賦值操做多線程

c.java.concurrent.Atomic.* 包中全部類的一切操做併發

(2)可見性ide

定義:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。性能

在多線程環境下,一個線程對共享變量的操做對其餘線程是不可見的。Java提供了volatile來保證可見性,當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會當即被更新到主內存中,其餘線程讀取共享變量時,會直接從主內存中讀取。固然,synchronize和Lock均可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。

(3)有序性

定義:即程序執行的順序按照代碼的前後順序執行。

Java內存模型中的有序性能夠總結爲:若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存主主內存同步延遲」現象。

在Java內存模型中,爲了效率是容許編譯器和處理器對指令進行重排序,固然重排序不會影響單線程的運行結果,可是對多線程會有影響。Java提供volatile來保證必定的有序性。最著名的例子就是單例模式裏面的DCL(雙重檢查鎖)。另外,能夠經過synchronized和Lock來保證有序性,synchronized和Lock保證每一個時刻是有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼,天然就保證了有序性。

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

(1)互斥即一次只容許一個線程持有某個特定的鎖,一次就只有一個線程可以使用該共享數據。

(2)可見性要更加複雜一些,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的。也即當一條線程修改了共享變量的值,新值對於其餘線程來講是能夠當即得知的。若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

a.對變量的寫操做不依賴於當前值。

b.該變量沒有包含在具備其餘變量的不變式中。

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

4、Java的內存模型JMM以及共享變量的可見性
JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。
Java中volatile關鍵字
對於普通的共享變量來說,線程A將其修改成某個值發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B已經緩存了該變量的舊值,因此就致使了共享變量值的不一致。解決這種共享變量在多線程模型中的不可見性問題,較粗暴的方式天然就是加鎖,可是此處使用synchronized或者Lock這些方式過重量級了,比較合理的方式其實就是volatile。

須要注意的是,JMM是個抽象的內存模型,因此所謂的本地內存,主內存都是抽象概念,並不必定就真實的對應cpu緩存和物理內存

5、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。
Java中volatile關鍵字
使用volatile關鍵字修飾共享變量即可以禁止這種重排序。若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序,volatile禁止指令重排序也有一些規則:

a.當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後

面的操做可見;在其後面的操做確定尚未進行;

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

到其前面執行。

即執行到volatile變量時,其前面的全部語句都執行完,後面全部語句都未執行。且前面語句的結果對volatile變

量及其後面語句可見。

6、volatile不適用的場景
(1)volatile不適合複合操做

例如,inc++不是一個原子性操做,能夠由讀取、加、賦值3步組成,因此結果並不能達到30000。.
Java中volatile關鍵字
(2)解決方法

1.採用synchronized
Java中volatile關鍵字
2.採用Lock
Java中volatile關鍵字
3.採用java併發包中的原子操做類,原子操做類是經過CAS循環的方式來保證其原子性的
Java中volatile關鍵字
7、volatile原理
volatile能夠保證線程可見性且提供了必定的有序性,可是沒法保證原子性。在JVM底層volatile是採用「內存屏障」來實現的。觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令,lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

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

存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

II. 它會強制將對緩存的修改操做當即寫入主存;

III. 若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

8、單例模式的雙重鎖爲何要加volatile
Java中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行並返回一個未初始化的對象。

原文連接:https://blog.csdn.net/u012723673/article/details/80682208

相關文章
相關標籤/搜索