摘要:線程在工作時,需要將主內存中的數據拷貝到工作內存中。內存可見性的應用當我們需要在兩個線程間依據主內存通信時,通信的那個變量就必須的用來修飾正在運行。。。
前言
不管是在面試還是實際開發中 volatile 都是一個應該掌握的技能。
首先來看看為什么會出現這個關鍵字。
內存可見性由于 Java 內存模型(JMM)規定,所有的變量都存放在主內存中,而每個線程都有著自己的工作內存(高速緩存)。
線程在工作時,需要將主內存中的數據拷貝到工作內存中。這樣對數據的任何操作都是基于工作內存(效率提高),并且不能直接操作主內存以及其他線程工作內存中的數據,之后再將更新之后的數據刷新到主內存中。
這里所提到的主內存可以簡單認為是堆內存,而工作內存則可以認為是棧內存。
如下圖所示:
所以在并發運行時可能會出現線程 B 所讀取到的數據是線程 A 更新之前的數據。
顯然這肯定是會出問題的,因此 volatile 的作用出現了:
當一個變量被 volatile 修飾時,任何線程對它的寫操作都會立即刷新到主內存中,并且會強制讓緩存了該變量的線程中的數據清空,必須從主內存重新讀取最新數據。
volatile 修飾之后并不是讓線程直接從主內存中獲取數據,依然需要將變量拷貝到工作內存中。
內存可見性的應用當我們需要在兩個線程間依據主內存通信時,通信的那個變量就必須的用 volatile 來修飾:
public class Volatile implements Runnable{ private static volatile boolean flag = true ; @Override public 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() ; @Override public 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 Mapvalue ; private static volatile boolean flag = fasle ; //以下方法發生在線程 A 中 初始化 Map public void initMap(){ //耗時操作 value = getMapValue() ;//1 flag = true ;//2 } //發生在線程 B中 等到 Map 初始化成功進行其他操作 public void doSomeThing(){ while(!flag){ sleep() ; } //dosomething doSomeThing(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 = new Singleton();,這段代碼其實是分為三步:
分配內存空間。(1)
初始化對象。(2)
將 singleton 對象指向分配的內存地址。(3)
加上 volatile 是為了讓以上的三步操作順序執行,反之有可能第二步在第三步之前被執行就有可能某個線程拿到的單例對象是還沒有初始化的,以致于報錯。
總結volatile 在 Java 并發中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定義為 volatile 來用于保證內存可見性。
將這塊理解透徹對我們編寫并發程序時可以提供很大幫助。
號外最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。
地址: https://github.com/crossoverJie/Java-Interview
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68710.html
摘要:當一個線程持有重量級鎖時,另外一個線程就會被直接踢到同步隊列中等待。 java代碼先編譯成字節碼,字節碼最后編譯成cpu指令,因此Java的多線程實現最終依賴于jvm和cpu的實現 synchronized和volatile 我們先來討論一下volatile關鍵字的作用以及實現機制,每個線程看到的用volatile修飾的變量的值都是最新的,更深入的解釋就涉及到Java的內存模型了,我們...
摘要:如線程執行后,線程執行,相當于線程向線程發送了消息。我們可以利用這種互斥性來進行線程間通信。 你是否真正理解并會用volatile, synchronized, final進行線程間通信呢,如果你不能回答下面的幾個問題,那就說明你并沒有真正的理解: 對volatile變量的操作一定具有原子性嗎? synchronized所謂的加鎖,鎖住的是什么? final定義的變量不變的到底是什么...
摘要:我們使用命令查看字節碼會發現在虛擬機中這個自增運算使用了條指令。其實這么說也不是最嚴謹的,因為即使經過編譯后的字節碼只使用了一條指令進行運算也不代表這條指令就是原子操作。 volatile的語義:1、保證被volatile修飾的變量對所有其他的線程的可見性。2、使用volatile修飾的變量禁止指令重排優化。看代碼: public class InheritThreadClass ex...
摘要:阿里開始招實習,同學問我要不要去申請阿里的實習,我說不去,個人對阿里的印象不好。記得去年阿里給我發了郵件,我很認真地回復,然后他不理我了。 引言 最近好久沒有遇到技術瓶頸了,思考得自然少了,每天都是重復性的工作。 阿里開始招實習,同學問我要不要去申請阿里的實習,我說不去,個人對阿里的印象不好。 記得去年阿里給我發了郵件,我很認真地回復,然后他不理我了。(最起碼的尊重都沒有,就算我菜你起...
摘要:假設不發生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時刷新到主存中。線程的最后操作與線程發現線程已經結束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關于 Java 并發也算是寫了好幾篇文章了,本文將介紹一些比較基礎的內容,注意,閱讀本文需要一定的并發基礎。 本文的...
閱讀 2722·2021-11-11 17:21
閱讀 613·2021-09-23 11:22
閱讀 3578·2019-08-30 15:55
閱讀 1641·2019-08-29 17:15
閱讀 573·2019-08-29 16:38
閱讀 904·2019-08-26 11:54
閱讀 2504·2019-08-26 11:53
閱讀 2750·2019-08-26 10:31