volatile是Java提供的一種輕量級的同步機制。Java 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量,相比于synchronized(synchronized通常稱為重量級鎖),volatile更輕量級,因為它不會引起線程上下文的切換和調度。但是volatile 變量的同步性較差(有時它更簡單并且開銷更低),而且其使用也更容易出錯。
volatile關鍵字的作用:保證了變量的可見性(visibility)。被volatile關鍵字修飾的變量,如果值發生了變更,其他線程立馬可見,避免出現臟讀的現象。
一、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行并返回一個未初始化的對象。