你应该知道的 volatile 关键字
前言
不管是在面試還是實際開發中?volatile?都是一個應該掌握的技能。
首先來看看為什么會出現這個關鍵字。
?
內存可見性
由于?Java?內存模型(?JMM)規定,所有的變量都存放在主內存中,而每個線程都有著自己的工作內存(高速緩存)。
線程在工作時,需要將主內存中的數據拷貝到工作內存中。這樣對數據的任何操作都是基于工作內存(效率提高),并且不能直接操作主內存以及其他線程工作內存中的數據,之后再將更新之后的數據刷新到主內存中。
這里所提到的主內存可以簡單認為是堆內存,而工作內存則可以認為是棧內存。
如下圖所示:
所以在并發運行時可能會出現線程 B 所讀取到的數據是線程 A 更新之前的數據。
顯然這肯定是會出問題的,因此?volatile?的作用出現了:
當一個變量被?volatile?修飾時,任何線程對它的寫操作都會立即刷新到主內存中,并且會強制讓緩存了該變量的線程中的數據清空,必須從主內存重新讀取最新數據。
volatile?修飾之后并不是讓線程直接從主內存中獲取數據,依然需要將變量拷貝到工作內存中。
?
內存可見性的應用
當我們需要在兩個線程間依據主內存通信時,通信的那個變量就必須的用?volatile?來修飾:
public class Volatile implements Runnable{private static volatile boolean flag = true ;@Overridepublic void run() {while (flag){System.out.println(Thread.currentThread().getName() + "正在運行。。。");}System.out.println(Thread.currentThread().getName() +"執行完畢");}public static void main(String[] args) throws InterruptedException {Volatile aVolatile = new Volatile();new Thread(aVolatile,"thread A").start();System.out.println("main 線程正在運行") ;TimeUnit.MILLISECONDS.sleep(100) ;aVolatile.stopThread();}private void stopThread(){flag = false ;}}主線程在修改了標志位使得線程 A 立即停止,如果沒有用?volatile?修飾,就有可能出現延遲。
但這里有個誤區,這樣的使用方式容易給人的感覺是:
對?volatile?修飾的變量進行并發操作是線程安全的。
這里要重點強調,?volatile?并不能保證線程安全性!
如下程序:
public class VolatileInc implements Runnable{private static volatile int count = 0 ; //使用 volatile 修飾基本數據內存不能保證原子性//private static AtomicInteger count = new AtomicInteger() ;@Overridepublic void run() {for (int i=0;i<10000 ;i++){count ++ ;//count.incrementAndGet() ;}}public static void main(String[] args) throws InterruptedException {VolatileInc volatileInc = new VolatileInc() ;Thread t1 = new Thread(volatileInc,"t1") ;Thread t2 = new Thread(volatileInc,"t2") ;t1.start();//t1.join();t2.start();//t2.join();for (int i=0;i<10000 ;i++){count ++ ;//count.incrementAndGet();}System.out.println("最終Count="+count);}}當我們三個線程(t1,t2,main)同時對一個?int?進行累加時會發現最終的值都會小于 30000。
這是因為雖然?volatile?保證了內存可見性,每個線程拿到的值都是最新值,但?count++?這個操作并不是原子的,這里面涉及到獲取值、自增、賦值的操作并不能同時完成。
-
所以想到達到線程安全可以使這三個線程串行執行(其實就是單線程,沒有發揮多線程的優勢)。
-
也可以使用?synchronize?或者是鎖的方式來保證原子性。
-
還可以用?Atomic?包中?AtomicInteger?來替換?int,它利用了?CAS?算法來保證了原子性。
?
指令重排
內存可見性只是?volatile?的其中一個語義,它還可以防止?JVM?進行指令重排優化。
舉一個偽代碼:
int a=10 ;//1 int b=20 ;//2 int c= a+b ;//3一段特別簡單的代碼,理想情況下它的執行順序是:?1>2>3。但有可能經過 JVM 優化之后的執行順序變為了?2>1>3。
可以發現不管 JVM 怎么優化,前提都是保證單線程中最終結果不變的情況下進行的。
可能這里還看不出有什么問題,那看下一段偽代碼:
private static Map<String,String> value ; private static volatile boolean flag = false ;//以下方法發生在線程 A 中 初始化 Map public void initMap(){//耗時操作value = getMapValue() ;//1flag = true ;//2 }//發生在線程 B中 等到 Map 初始化成功進行其他操作 public void doSomeThing(){while(!flag){sleep() ;}//dosomethingdoSomeThing(value);}這里就能看出問題了,當?flag?沒有被?volatile?修飾時,?JVM?對 1 和 2 進行重排,導致?value?都還沒有被初始化就有可能被線程 B 使用了。
所以加上?volatile?之后可以防止這樣的重排優化,保證業務的正確性。
?
指令重排的的應用
一個經典的使用場景就是雙重懶加載的單例模式了:
public class Singleton {private static volatile Singleton singleton;private Singleton() {}public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {//防止指令重排singleton = new Singleton();}}}return singleton;}}這里的?volatile?關鍵字主要是為了防止指令重排。
如果不用 ,?singleton = newSingleton();,這段代碼其實是分為三步:
-
分配內存空間。(1)
-
初始化對象。(2)
-
將?singleton?對象指向分配的內存地址。(3)
加上?volatile?是為了讓以上的三步操作順序執行,反之有可能第三步在第二步之前被執行,就有可能某個線程拿到的單例對象是還沒有初始化的,以致于報錯。
?
總結
volatile?在?Java?并發中用的很多,比如像?Atomic?包中的?value、以及?AbstractQueuedLongSynchronizer中的?state?都是被定義為?volatile?來用于保證內存可見性。
將這塊理解透徹對我們編寫并發程序時可以提供很大幫助。
總結
以上是生活随笔為你收集整理的你应该知道的 volatile 关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里巴巴P9大佬雷卷与中间件小哥重新定义
- 下一篇: 深入理解线程通信