Java程序員面試:Volatile全方位解析
前言
volatile是Java程序員必備的基礎(chǔ),也是面試官非常喜歡問的一個(gè)話題,本文跟大家一起開啟vlatile學(xué)習(xí)之旅,如果有不正確的地方,也麻煩大家指出哈,一起相互學(xué)習(xí)~
- 1.volatile的用法
- 2.vlatile變量的作用
- 3.現(xiàn)代計(jì)算機(jī)的內(nèi)存模型(計(jì)算機(jī)模型,總線,MESI協(xié)議,嗅探技術(shù))
- 4.Java內(nèi)存模型(JMM)
- 5.并發(fā)編程的3個(gè)特性(原子性、可見性、有序性、happen-before、as-if-serial、指令重排)
- 6.volatile的底層原理(如何保證可見性,如何保證指令重排,內(nèi)存屏障)
- 7.volatile的典型場(chǎng)景(狀態(tài)標(biāo)志,DCL單例模式)
- 8.volatile常見面試題&&答案解析
「github 地址」
❝https://github.com/whx123/JavaHome❞
1.volatile的用法
volatile關(guān)鍵字是Java虛擬機(jī)提供的的「最輕量級(jí)的同步機(jī)制」,它作為一個(gè)修飾符出現(xiàn),用來「修飾變量」,但是這里不包括局部變量哦。我們來看個(gè)demo吧,代碼如下:
- /**
- * @Author 撿田螺的小男孩
- * @Date 2020/08/02
- * @Desc volatile的可見性探索
- */
- public class VolatileTest {
- public static void main(String[] args) throws InterruptedException {
- Task task = new Task();
- Thread t1 = new Thread(task, "線程t1");
- Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- Thread.sleep(1000);
- System.out.println("開始通知線程停止");
- task.stop = true; //修改stop變量值。
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }, "線程t2");
- t1.start(); //開啟線程t1
- t2.start(); //開啟線程t2
- Thread.sleep(1000);
- }
- }
- class Task implements Runnable {
- boolean stop = false;
- int i = 0;
- @Override
- public void run() {
- long s = System.currentTimeMillis();
- while (!stop) {
- i++;
- }
- System.out.println("線程退出" + (System.currentTimeMillis() - s));
- }
- }
「運(yùn)行結(jié)果:」
可以發(fā)現(xiàn)線程t2,雖然把stop設(shè)置為true了,但是線程t1對(duì)t2的「stop變量視而不可見」,因此,它一直在死循環(huán)running中。如果給變量stop加上volatile修飾,線程t1是可以停下來的,運(yùn)行結(jié)果如下:
- volatile boolean stop = false;
2. vlatile修飾變量的作用
從以上例子,我們可以發(fā)現(xiàn)變量stop,加了vlatile修飾之后,線程t1對(duì)stop就可見了。其實(shí),vlatile的作用就是:「保證變量對(duì)所有線程可見性」。當(dāng)然,vlatile還有個(gè)作用就是,「禁止指令重排」,但是它「不保證原子性」。
所以當(dāng)面試官問你「volatile的作用或者特性」,都可以這么回答:
保證變量對(duì)所有線程可見性;
禁止指令重排序
不保證原子性
3. 現(xiàn)代計(jì)算機(jī)的內(nèi)存模型(計(jì)算機(jī)模型,MESI協(xié)議,嗅探技術(shù),總線)
為了更好理解volatile,先回顧一下計(jì)算機(jī)的內(nèi)存模型與JMM(Java內(nèi)存模型)吧~
計(jì)算機(jī)模型計(jì)算機(jī)執(zhí)行程序時(shí),指令是由CPU處理器執(zhí)行的,而打交道的數(shù)據(jù)是在主內(nèi)存當(dāng)中的。
由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,總不能每次CPU執(zhí)行完指令,然后等主內(nèi)存慢悠悠存取數(shù)據(jù)吧, 所以現(xiàn)代計(jì)算機(jī)系統(tǒng)加入一層讀寫速度接近處理器運(yùn)算速度的高速緩存(Cache),以作為來作為內(nèi)存與處理器之間的緩沖。
在多路處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們共享同一主內(nèi)存。「計(jì)算機(jī)抽象內(nèi)存模型」如下:
- 程序執(zhí)行時(shí),把需要用到的數(shù)據(jù),從主內(nèi)存拷貝一份到高速緩存。
- CPU處理器計(jì)算時(shí),從它的高速緩存中讀取,把計(jì)算完的數(shù)據(jù)寫入高速緩存。
- 當(dāng)程序運(yùn)算結(jié)束,把高速緩存的數(shù)據(jù)刷新會(huì)主內(nèi)存。
隨著科學(xué)技術(shù)的發(fā)展,為了效率,高速緩存又衍生出一級(jí)緩存(L1),二級(jí)緩存(L2),甚至三級(jí)緩存(L3);
當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域,可能導(dǎo)致「緩存數(shù)據(jù)不一致」問題。如何解決這個(gè)問題呢?有兩種方案
❝1、通過在總線加LOCK#鎖的方式。
2、通過緩存一致性協(xié)議(Cache Coherence Protocol)❞
總線
❝總線(Bus)是計(jì)算機(jī)各種功能部件之間傳送信息的公共通信干線,它是由導(dǎo)線組成的傳輸線束, 按照計(jì)算機(jī)所傳輸?shù)男畔⒎N類,計(jì)算機(jī)的總線可以劃分為數(shù)據(jù)總線、地址總線和控制總線,分別用來傳輸數(shù)據(jù)、數(shù)據(jù)地址和控制信號(hào)。❞
CPU和其他功能部件是通過總線通信的,如果在總線加LOCK#鎖,那么在鎖住總線期間,其他CPU是無法訪問內(nèi)存,這樣一來,「效率就比較低了」。
MESI協(xié)議為了解決一致性問題,還可以通過緩存一致性協(xié)議。即各個(gè)處理器訪問緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。比較著名的就是Intel的MESI(Modified Exclusive Shared Or Invalid)協(xié)議,它的核心思想是:
❝當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的,那么它就會(huì)從內(nèi)存重新讀取。❞
CPU中每個(gè)緩存行標(biāo)記的4種狀態(tài)(M、E、S、I),也了解一下吧:
緩存狀態(tài) | 描述 |
---|---|
M,被修改(Modified) | 該緩存行只被該CPU緩存,與主存的值不同,會(huì)在它被其他CPU讀取之前寫入內(nèi)存,并設(shè)置為Shared |
E,獨(dú)享的(Exclusive) | 該緩存行只被該CPU緩存,與主存的值相同,被其他CPU讀取時(shí)置為Shared,被其他CPU寫時(shí)置為Modified |
S,共享的(Shared) | 該緩存行可能被多個(gè)CPU緩存,各個(gè)緩存中的數(shù)據(jù)與主存數(shù)據(jù)相同 |
I,無效的(Invalid) | 該緩存行數(shù)據(jù)是無效,需要時(shí)需重新從主存載入 |
MESI協(xié)議是如何實(shí)現(xiàn)的?如何保證當(dāng)前處理器的內(nèi)部緩存、主內(nèi)存和其他處理器的緩存數(shù)據(jù)在總線上保持一致的?「多處理器總線嗅探」
嗅探技術(shù)
❝在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存緩存一致性協(xié)議,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存值是不是過期了,如果處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)庫(kù)讀到處理器緩存中。❞
4. Java內(nèi)存模型(JMM)
- Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型,來「屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異」,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問效果。
- Java內(nèi)存模型「類比」于計(jì)算機(jī)內(nèi)存模型。
- 為了更好的執(zhí)行性能,java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存打交道,也沒有限制編譯器進(jìn)行調(diào)整代碼順序優(yōu)化。所以Java內(nèi)存模型「會(huì)存在緩存一致性問題和指令重排序問題的」。
- Java內(nèi)存模型規(guī)定所有的變量都是存在主內(nèi)存當(dāng)中(類似于計(jì)算機(jī)模型中的物理內(nèi)存),每個(gè)線程都有自己的工作內(nèi)存(類似于計(jì)算機(jī)模型的高速緩存)。這里的「變量」包括實(shí)例變量和靜態(tài)變量,但是「不包括局部變量」,因?yàn)榫植孔兞渴蔷€程私有的。
- 線程的工作內(nèi)存保存了被該線程使用的變量的主內(nèi)存副本,「線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行」,而不能直接操作操作主內(nèi)存。并且每個(gè)線程不能訪問其他線程的工作內(nèi)存。
舉個(gè)例子吧,假設(shè)i的初始值是0,執(zhí)行以下語句:
- i = i+1;
首先,執(zhí)行線程t1從主內(nèi)存中讀取到i=0,到工作內(nèi)存。然后在工作內(nèi)存中,賦值i+1,工作內(nèi)存就得到i=1,最后把結(jié)果寫回主內(nèi)存。因此,如果是單線程的話,該語句執(zhí)行是沒問題的。但是呢,線程t2的本地工作內(nèi)存還沒過期,那么它讀到的數(shù)據(jù)就是臟數(shù)據(jù)了。如圖:
Java內(nèi)存模型是圍繞著如何在并發(fā)過程中如何處理「原子性、可見性和有序性」這3個(gè)特征來建立的,我們?cè)賮硪黄鸹仡櫼幌聗
5.并發(fā)編程的3個(gè)特性(原子性、可見性、有序性)
原子性
原子性,指操作是不可中斷的,要么執(zhí)行完成,要么不執(zhí)行,基本數(shù)據(jù)類型的訪問和讀寫都是具有原子性,當(dāng)然(long和double的非原子性協(xié)定除外)。我們來看幾個(gè)小例子:
- i =666; // 語句1
- i = j; // 語句2
- i = i+1; //語句 3
- i++; // 語句4
- 語句1操作顯然是原子性的,將數(shù)值666賦值給i,即線程執(zhí)行這個(gè)語句時(shí),直接將數(shù)值666寫入到工作內(nèi)存中。
- 語句2操作看起來也是原子性的,但是它實(shí)際上涉及兩個(gè)操作,先去讀j的值,再把j的值寫入工作內(nèi)存,兩個(gè)操作分開都是原子操作,但是合起來就不滿足原子性了。
- 語句3讀取i的值,加1,再寫回主存,這個(gè)就不是原子性操作了。
- 語句4 等同于語句3,也是非原子性操作。
可見性
- 可見性就是指當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改。
- Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性的,無論是普通變量還是volatile變量都是如此。
- volatile變量,保證新值能立即同步回主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新,所以我們說volatile保證了多線程操作變量的可見性。
- synchronized和Lock也能夠保證可見性,線程在釋放鎖之前,會(huì)把共享變量值都刷回主存。final也可以實(shí)現(xiàn)可見性。
有序性
Java虛擬機(jī)這樣描述Java程序的有序性的:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程中,觀察另一個(gè)線程,所有的操作都是無序的。
后半句意思就是,在Java內(nèi)存模型中,「允許編譯器和處理器對(duì)指令進(jìn)行重排序」,會(huì)影響到多線程并發(fā)執(zhí)行的正確性;前半句意思就是「as-if-serial」的語義,即不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不會(huì)被改變。
比如以下程序代碼:
- double pi = 3.14; //A
- double r = 1.0; //B
- double area = pi * r * r; //C
步驟C依賴于步驟A和B,因?yàn)橹噶钪嘏诺拇嬖冢绦驁?zhí)行順訊可能是A->B->C,也可能是B->A->C,但是C不能在A或者B前面執(zhí)行,這將違反as-if-serial語義。
看段代碼吧,假設(shè)程序先執(zhí)行read方法,再執(zhí)行add方法,結(jié)果一定是輸出sum=2嘛?
- bool flag = false;
- int b = 0;
- public void read() {
- b = 1; //1
- flag = true; //2
- }
- public void add() {
- if (flag) { //3
- int sum =b+b; //4
- System.out.println("bb sum is"+sum);
- }
- }
如果是單線程,結(jié)果應(yīng)該沒問題,如果是多線程,線程t1對(duì)步驟1和2進(jìn)行了「指令重排序」呢?結(jié)果sum就不是2了,而是0,如下圖所示:
這是為啥呢?「指令重排序」了解一下,指令重排是指在程序執(zhí)行過程中,「為了提高性能」, 「編譯器和CPU可能會(huì)對(duì)指令進(jìn)行重新排序」。CPU重排序包括指令并行重排序和內(nèi)存系統(tǒng)重排序,重排序類型和重排序執(zhí)行過程如下:
實(shí)際上,可以給flag加上volatile關(guān)鍵字,來保證有序性。當(dāng)然,也可以通過synchronized和Lock來保證有序性。synchronized和Lock保證某一時(shí)刻是只有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行程序代碼了,自然就保證了有序性。
實(shí)際上Java內(nèi)存模型的有序性并不是僅靠volatile、synchronized和Lock來保證有序性的。這是因?yàn)镴ava語言中,有一個(gè)先行發(fā)生原則(happens-before):
- 「程序次序規(guī)則」:在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 「管程鎖定規(guī)則」:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作
- 「volatile變量規(guī)則」:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作
- 「線程啟動(dòng)規(guī)則」:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作
- 「線程終止規(guī)則」:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
- 「線程中斷規(guī)則」:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
- 「對(duì)象終結(jié)規(guī)則」:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始
- 「?jìng)鬟f性」:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
根據(jù)happens-before的八大規(guī)則,我們回到剛的例子,一起分析一下。給flag加上volatile關(guān)鍵字,look look它是如何保證有序性的,
- volatile bool flag = false;
- int b = 0;
- public void read() {
- b = 1; //1
- flag = true; //2
- }
- public void add() {
- if (flag) { //3
- int sum =b+b; //4
- System.out.println("bb sum is"+sum);
- }
- }
- 首先呢,flag加上volatile關(guān)鍵字,那就禁止了指令重排,也就是1 happens-before 2了
- 根據(jù)「volatile變量規(guī)則」,2 happens-before 3
- 由「程序次序規(guī)則」,得出 3 happens-before 4
- 最后由「?jìng)鬟f性」,得出1 happens-before 4,因此妥妥的輸出sum=2啦~
6.volatile底層原理
以上討論學(xué)習(xí),我們知道volatile的語義就是保證變量對(duì)所有線程可見性以及禁止指令重排優(yōu)化。那么,它的底層是如何保證可見性和禁止指令重排的呢?
圖解volatile是如何保證可見性的?
在這里,先看幾個(gè)圖吧,哈哈~
假設(shè)flag變量的初始值false,現(xiàn)在有兩條線程t1和t2要訪問它,就可以簡(jiǎn)化為以下圖:
如果線程t1執(zhí)行以下代碼語句,并且flag沒有volatile修飾的話;t1剛修改完flag的值,還沒來得及刷新到主內(nèi)存,t2又跑過來讀取了,很容易就數(shù)據(jù)flag不一致了,如下:
- flag=true;
如果flag變量是由volatile修飾的話,就不一樣了,如果線程t1修改了flag值,volatile能保證修飾的flag變量后,可以「立即同步回主內(nèi)存」。如圖:
細(xì)心的朋友會(huì)發(fā)現(xiàn),線程t2不還是flag舊的值嗎,這不還有問題嘛?其實(shí)volatile還有一個(gè)保證,就是「每次使用前立即先從主內(nèi)存刷新最新的值」,線程t1修改完后,線程t2的變量副本會(huì)過期了,如圖:
顯然,這里還不是底層,實(shí)際上volatile保證可見性和禁止指令重排都跟「內(nèi)存屏障」有關(guān),我們編譯volatile相關(guān)代碼看看~
DCL單例模式(volatile)&編譯對(duì)比
DCL單例模式(Double Check Lock,雙重檢查鎖)比較常用,它是需要volatile修飾的,所以就拿這段代碼編譯吧
- public class Singleton {
- private volatile static Singleton instance;
- private Singleton (){}
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
編譯這段代碼后,觀察有volatile關(guān)鍵字和沒有volatile關(guān)鍵字時(shí)的instance所生成的匯編代碼發(fā)現(xiàn),有volatile關(guān)鍵字修飾時(shí),會(huì)多出一個(gè)lock addl $0x0,(%esp),即多出一個(gè)lock前綴指令
- 0x01a3de0f: mov $0x3375cdb0,%esi ;...beb0cd75 33
- ; {oop('Singleton')}
- 0x01a3de14: mov %eax,0x150(%esi) ;...89865001 0000
- 0x01a3de1a: shr $0x9,%esi ;...c1ee09
- 0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100
- 0x01a3de24: lock addl $0x0,(%esp) ;...f0830424 00
- ;*putstatic instance
- ; - Singleton::getInstance@24
lock指令相當(dāng)于一個(gè)「內(nèi)存屏障」,它保證以下這幾點(diǎn):
- ❝1.重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
- 2.將本處理器的緩存寫入內(nèi)存
- 3.如果是寫入動(dòng)作,會(huì)導(dǎo)致其他處理器中對(duì)應(yīng)的緩存無效。❞
顯然,第2、3點(diǎn)不就是volatile保證可見性的體現(xiàn)嘛,第1點(diǎn)就是禁止指令重排列的體現(xiàn)。
內(nèi)存屏障內(nèi)存屏障四大分類:(Load 代表讀取指令,Store代表寫入指令)
內(nèi)存屏障
內(nèi)存屏障類型 | 抽象場(chǎng)景 | 描述 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 在Load2要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。 |
StoreStore屏障 | Store1; StoreStore; Store2 | 在Store2寫入執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見 |
LoadStore屏障 | Load1; LoadStore; Store2 | 在Store2被寫入前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 在Load2讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見。 |
為了實(shí)現(xiàn)volatile的內(nèi)存語義,Java內(nèi)存模型采取以下的保守策略
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的前面插入一個(gè)LoadLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
有些小伙伴,可能對(duì)這個(gè)還是有點(diǎn)疑惑,內(nèi)存屏障這玩意太抽象了。我們照著代碼看下吧:
內(nèi)存屏障保證前面的指令先執(zhí)行,所以這就保證了禁止了指令重排啦,同時(shí)內(nèi)存屏障保證緩存寫入內(nèi)存和其他處理器緩存失效,這也就保證了可見性,哈哈~
7.volatile的典型場(chǎng)景
通常來說,使用volatile必須具備以下2個(gè)條件:
- 1)對(duì)變量的寫操作不依賴于當(dāng)前值
- 2)該變量沒有包含在具有其他變量的不變式中
實(shí)際上,volatile場(chǎng)景一般就是「狀態(tài)標(biāo)志」,以及「DCL單例模式」。
7.1 狀態(tài)標(biāo)志
深入理解Java虛擬機(jī),書中的例子:
- Map configOptions;
- char[] configText;
- // 此變量必須定義為 volatile
- volatile boolean initialized = false;
- // 假設(shè)以下代碼在線程 A 中運(yùn)行
- // 模擬讀取配置信息, 當(dāng)讀取完成后將 initialized 設(shè)置為 true 以告知其他線程配置可用
- configOptions = new HashMap();
- configText = readConfigFile(fileName);
- processConfigOptions(configText, configOptions);
- initialized = true;
- // 假設(shè)以下代碼在線程 B 中運(yùn)行
- // 等待 initialized 為 true, 代表線程 A 已經(jīng)把配置信息初始化完成
- while(!initialized) {
- sleep();
- }
- // 使用線程 A 中初始化好的配置信息
- doSomethingWithConfig();
7.2 DCL單例模式
- class Singleton{
- private volatile static Singleton instance = null;
- private Singleton() {
- }
- public static Singleton getInstance() {
- if(instance==null) {
- synchronized (Singleton.class) {
- if(instance==null)
- instance = new Singleton();
- }
- }
- return instance;
- }
- }
8. volatile相關(guān)經(jīng)典面試題
- 談?wù)剉olatile的特性
- volatile的內(nèi)存語義
- 說說并發(fā)編程的3大特性
- 什么是內(nèi)存可見性,什么是指令重排序?
- volatile是如何解決java并發(fā)中可見性的問題
- volatile如何防止指令重排
- volatile可以解決原子性嘛?為什么?
- volatile底層的實(shí)現(xiàn)機(jī)制
- volatile和synchronized的區(qū)別?
8.1 談?wù)剉olatile的特性
8.2 volatile的內(nèi)存語義
當(dāng)寫一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
當(dāng)讀一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
8.3 說說并發(fā)編程的3大特性
- 原子性
- 可見性
- 有序性
8.4 什么是內(nèi)存可見性,什么是指令重排序?
可見性就是指當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改。
指令重排是指JVM在編譯Java代碼的時(shí)候,或者CPU在執(zhí)行JVM字節(jié)碼的時(shí)候,對(duì)現(xiàn)有的指令順序進(jìn)行重新排序。
8.5 volatile是如何解決java并發(fā)中可見性的問題
底層是通過內(nèi)存屏障實(shí)現(xiàn)的哦,volatile能保證修飾的變量后,可以立即同步回主內(nèi)存,每次使用前立即先從主內(nèi)存刷新最新的值。
8.6 volatile如何防止指令重排
也是內(nèi)存屏障哦,跟面試官講下Java內(nèi)存的保守策略:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的前面插入一個(gè)LoadLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
再講下volatile的語義哦,重排序時(shí)不能把內(nèi)存屏障后面的指令重排序到內(nèi)存屏障之前的位置
8.7 volatile可以解決原子性嘛?為什么?不可以,可以直接舉i++那個(gè)例子,原子性需要synchronzied或者lock保證
- public class Test {
- public volatile int race = 0;
- public void increase() {
- race++;
- }
- public static void main(String[] args) {
- final Test test = new Test();
- for(int i=0;i<10;i++){
- new Thread(){
- public void run() {
- for(int j=0;j<100;j++)
- test.increase();
- };
- }.start();
- }
- //等待所有累加線程結(jié)束
- while(Thread.activeCount()>1)
- Thread.yield();
- System.out.println(test.race);
- }
- }
8.8 volatile底層的實(shí)現(xiàn)機(jī)制
可以看本文的第六小節(jié),volatile底層原理哈,主要你要跟面試官講述,volatile如何保證可見性和禁止指令重排,需要講到內(nèi)存屏障~
8.9 volatile和synchronized的區(qū)別?
- volatile修飾的是變量,synchronized一般修飾代碼塊或者方法
- volatile保證可見性、禁止指令重排,但是不保證原子性;synchronized可以保證原子性
- volatile不會(huì)造成線程阻塞,synchronized可能會(huì)造成線程的阻塞,所以后面才有鎖優(yōu)化那么多故事~
- 哈哈,你還有補(bǔ)充嘛~
本文轉(zhuǎn)載自微信公眾號(hào)「撿田螺的小男孩」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系撿田螺的小男孩公眾號(hào)。