摘要:線程在工作時,需要將主內(nèi)存中的數(shù)據(jù)拷貝到工作內(nèi)存中。內(nèi)存可見性的應(yīng)用當(dāng)我們需要在兩個線程間依據(jù)主內(nèi)存通信時,通信的那個變量就必須的用來修飾正在運行。。。
前言
不管是在面試還是實際開發(fā)中 volatile 都是一個應(yīng)該掌握的技能。
首先來看看為什么會出現(xiàn)這個關(guān)鍵字。
內(nèi)存可見性由于 Java 內(nèi)存模型(JMM)規(guī)定,所有的變量都存放在主內(nèi)存中,而每個線程都有著自己的工作內(nèi)存(高速緩存)。
線程在工作時,需要將主內(nèi)存中的數(shù)據(jù)拷貝到工作內(nèi)存中。這樣對數(shù)據(jù)的任何操作都是基于工作內(nèi)存(效率提高),并且不能直接操作主內(nèi)存以及其他線程工作內(nèi)存中的數(shù)據(jù),之后再將更新之后的數(shù)據(jù)刷新到主內(nèi)存中。
這里所提到的主內(nèi)存可以簡單認(rèn)為是堆內(nèi)存,而工作內(nèi)存則可以認(rèn)為是棧內(nèi)存。
如下圖所示:
所以在并發(fā)運行時可能會出現(xiàn)線程 B 所讀取到的數(shù)據(jù)是線程 A 更新之前的數(shù)據(jù)。
顯然這肯定是會出問題的,因此 volatile 的作用出現(xiàn)了:
當(dāng)一個變量被 volatile 修飾時,任何線程對它的寫操作都會立即刷新到主內(nèi)存中,并且會強制讓緩存了該變量的線程中的數(shù)據(jù)清空,必須從主內(nèi)存重新讀取最新數(shù)據(jù)。
volatile 修飾之后并不是讓線程直接從主內(nèi)存中獲取數(shù)據(jù),依然需要將變量拷貝到工作內(nèi)存中。
內(nèi)存可見性的應(yīng)用當(dāng)我們需要在兩個線程間依據(jù)主內(nèi)存通信時,通信的那個變量就必須的用 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() +"執(zhí)行完畢"); } 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 ; } }
主線程在修改了標(biāo)志位使得線程 A 立即停止,如果沒有用 volatile 修飾,就有可能出現(xiàn)延遲。
但這里有個誤區(qū),這樣的使用方式容易給人的感覺是:
對 volatile 修飾的變量進行并發(fā)操作是線程安全的。
這里要重點強調(diào),volatile 并不能保證線程安全性!
如下程序:
public class VolatileInc implements Runnable{ private static volatile int count = 0 ; //使用 volatile 修飾基本數(shù)據(jù)內(nèi)存不能保證原子性 //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); } }
當(dāng)我們?nèi)齻€線程(t1,t2,main)同時對一個 int 進行累加時會發(fā)現(xiàn)最終的值都會小于 30000。
這是因為雖然 volatile 保證了內(nèi)存可見性,每個線程拿到的值都是最新值,但 count ++ 這個操作并不是原子的,這里面涉及到獲取值、自增、賦值的操作并不能同時完成。
所以想到達(dá)到線程安全可以使這三個線程串行執(zhí)行(其實就是單線程,沒有發(fā)揮多線程的優(yōu)勢)。
也可以使用 synchronize 或者是鎖的方式來保證原子性。
還可以用 Atomic 包中 AtomicInteger 來替換 int,它利用了 CAS 算法來保證了原子性。
指令重排內(nèi)存可見性只是 volatile 的其中一個語義,它還可以防止 JVM 進行指令重排優(yōu)化。
舉一個偽代碼:
int a=10 ;//1 int b=20 ;//2 int c= a+b ;//3
一段特別簡單的代碼,理想情況下它的執(zhí)行順序是:1>2>3。但有可能經(jīng)過 JVM 優(yōu)化之后的執(zhí)行順序變?yōu)榱?2>1>3。
可以發(fā)現(xiàn)不管 JVM 怎么優(yōu)化,前提都是保證單線程中最終結(jié)果不變的情況下進行的。
可能這里還看不出有什么問題,那看下一段偽代碼:
private static Mapvalue ; private static volatile boolean flag = fasle ; //以下方法發(fā)生在線程 A 中 初始化 Map public void initMap(){ //耗時操作 value = getMapValue() ;//1 flag = true ;//2 } //發(fā)生在線程 B中 等到 Map 初始化成功進行其他操作 public void doSomeThing(){ while(!flag){ sleep() ; } //dosomething doSomeThing(value); }
這里就能看出問題了,當(dāng) flag 沒有被 volatile 修飾時,JVM 對 1 和 2 進行重排,導(dǎo)致 value 都還沒有被初始化就有可能被線程 B 使用了。
所以加上 volatile 之后可以防止這樣的重排優(yōu)化,保證業(yè)務(wù)的正確性。
指令重排的的應(yīng)用一個經(jīng)典的使用場景就是雙重懶加載的單例模式了:
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 關(guān)鍵字主要是為了防止指令重排。
如果不用 ,singleton = new Singleton();,這段代碼其實是分為三步:
分配內(nèi)存空間。(1)
初始化對象。(2)
將 singleton 對象指向分配的內(nèi)存地址。(3)
加上 volatile 是為了讓以上的三步操作順序執(zhí)行,反之有可能第二步在第三步之前被執(zhí)行就有可能某個線程拿到的單例對象是還沒有初始化的,以致于報錯。
總結(jié)volatile 在 Java 并發(fā)中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定義為 volatile 來用于保證內(nèi)存可見性。
將這塊理解透徹對我們編寫并發(fā)程序時可以提供很大幫助。
號外最近在總結(jié)一些 Java 相關(guān)的知識點,感興趣的朋友可以一起維護。
地址: https://github.com/crossoverJie/Java-Interview
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/68710.html
摘要:當(dāng)一個線程持有重量級鎖時,另外一個線程就會被直接踢到同步隊列中等待。 java代碼先編譯成字節(jié)碼,字節(jié)碼最后編譯成cpu指令,因此Java的多線程實現(xiàn)最終依賴于jvm和cpu的實現(xiàn) synchronized和volatile 我們先來討論一下volatile關(guān)鍵字的作用以及實現(xiàn)機制,每個線程看到的用volatile修飾的變量的值都是最新的,更深入的解釋就涉及到Java的內(nèi)存模型了,我們...
摘要:如線程執(zhí)行后,線程執(zhí)行,相當(dāng)于線程向線程發(fā)送了消息。我們可以利用這種互斥性來進行線程間通信。 你是否真正理解并會用volatile, synchronized, final進行線程間通信呢,如果你不能回答下面的幾個問題,那就說明你并沒有真正的理解: 對volatile變量的操作一定具有原子性嗎? synchronized所謂的加鎖,鎖住的是什么? final定義的變量不變的到底是什么...
摘要:我們使用命令查看字節(jié)碼會發(fā)現(xiàn)在虛擬機中這個自增運算使用了條指令。其實這么說也不是最嚴(yán)謹(jǐn)?shù)?,因為即使?jīng)過編譯后的字節(jié)碼只使用了一條指令進行運算也不代表這條指令就是原子操作。 volatile的語義:1、保證被volatile修飾的變量對所有其他的線程的可見性。2、使用volatile修飾的變量禁止指令重排優(yōu)化??创a: public class InheritThreadClass ex...
摘要:阿里開始招實習(xí),同學(xué)問我要不要去申請阿里的實習(xí),我說不去,個人對阿里的印象不好。記得去年阿里給我發(fā)了郵件,我很認(rèn)真地回復(fù),然后他不理我了。 引言 最近好久沒有遇到技術(shù)瓶頸了,思考得自然少了,每天都是重復(fù)性的工作。 阿里開始招實習(xí),同學(xué)問我要不要去申請阿里的實習(xí),我說不去,個人對阿里的印象不好。 記得去年阿里給我發(fā)了郵件,我很認(rèn)真地回復(fù),然后他不理我了。(最起碼的尊重都沒有,就算我菜你起...
摘要:假設(shè)不發(fā)生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時刷新到主存中。線程的最后操作與線程發(fā)現(xiàn)線程已經(jīng)結(jié)束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關(guān)于 Java 并發(fā)也算是寫了好幾篇文章了,本文將介紹一些比較基礎(chǔ)的內(nèi)容,注意,閱讀本文需要一定的并發(fā)基礎(chǔ)。 本文的...
閱讀 2740·2021-11-11 17:21
閱讀 629·2021-09-23 11:22
閱讀 3591·2019-08-30 15:55
閱讀 1654·2019-08-29 17:15
閱讀 586·2019-08-29 16:38
閱讀 922·2019-08-26 11:54
閱讀 2522·2019-08-26 11:53
閱讀 2768·2019-08-26 10:31