日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

你应该知道的 volatile 关键字

發布時間:2025/3/21 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 你应该知道的 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 关键字的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。