一、前言
volatile的關(guān)鍵詞的使用在JVM內(nèi)存模型中已是老生常談了,這篇文章主要結(jié)合自己對(duì)可見性的一些認(rèn)識(shí)和一些直觀的例子來談?wù)剉olatile。文章正文大致分為三部分,首先會(huì)介紹一下happen-before,接著講解volatile的一些使用場(chǎng)景,最后會(huì)附上一些例子來論證使用與不使用volatile的區(qū)別。
二、happen-before
對(duì)操作系統(tǒng)有認(rèn)識(shí)的同學(xué)一定知道,CPU一般有三級(jí)緩存,在與內(nèi)存交互的時(shí)候,存在緩存與內(nèi)存的更新問題,其次CPU在讀取指令的時(shí)候,會(huì)做一些指令重排序的工作,提高程序運(yùn)行效率。類比JVM內(nèi)存模型(見下圖),每個(gè)線程擁有自己的工作內(nèi)存,同時(shí)存在一個(gè)主存,線程間通過主存來進(jìn)行通信,同樣的,JVM也存在指令重排序,可見JVM內(nèi)存模型與實(shí)際物理內(nèi)存模型十分相似。(這里順便提一下,編譯器其實(shí)也會(huì)作一定重排序優(yōu)化)。
作為開發(fā)人員,你不可能了解到每個(gè)JVM優(yōu)化細(xì)節(jié),更不可能了解到CPU何時(shí)會(huì)進(jìn)行指令重排序,所以java語(yǔ)言定義了更上層的一個(gè)概念,就是"happen-before"。起初,我看到這個(gè)單詞的時(shí)候,誤以為這是一個(gè)指令執(zhí)行順序的規(guī)則,后來仔細(xì)想想又發(fā)覺不對(duì)勁。如果”happen-before“僅僅是抽象了指令執(zhí)行順序的概念,那么它就把握不了“工作內(nèi)存將值寫回主存”和“工作內(nèi)存從主存中刷新自己的值”這個(gè)兩個(gè)action的時(shí)機(jī)。那么這個(gè)概念也就變得沒什么意義了。所以!所以!所以!”happen-before“是一個(gè)可見性的原則!!!
下面給出happen-before的具體規(guī)則:
程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作;
volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作;
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作;
線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行;
對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始;
三、volatile的使用場(chǎng)景
happen-before的第三條規(guī)則提到“volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作”,也就是說;一個(gè)volatile變量的寫操作對(duì)后續(xù)對(duì)讀操作可見。說白了就是每次寫完volatile變量,都會(huì)將值從工作內(nèi)存寫回到主存中去,每次讀取volatile變量,工作內(nèi)存必須從主存中刷新下自己的值。如此的話,volatile就是為了解決多個(gè)線程共享數(shù)據(jù)的可見性問題。但是不是任何數(shù)據(jù)共享場(chǎng)景都可以使用volatile,必須滿足以下兩種情景才行。
應(yīng)用場(chǎng)景:
1.多個(gè)線程不依賴原值的情況下進(jìn)行讀寫操作
2.一個(gè)線程依賴原值進(jìn)行寫操作,多個(gè)線程進(jìn)行讀操作
在我看來,除了這兩種情況外,無非是多個(gè)線程依賴原值進(jìn)行運(yùn)算,這樣子倒不是說volatile可見性不起作用了,而是無法保證讀取原值和運(yùn)算是一個(gè)原子操作!舉個(gè)簡(jiǎn)單的例子,多個(gè)線程執(zhí)行i++;i是一個(gè)共享變量,由于讀取i的值和i自增不是一個(gè)原子操作,所以i最終會(huì)丟失掉一部分自增過程。代碼如下,最終i輸出的結(jié)果是一個(gè)小于1000的整數(shù)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/** * Created by chenqimiao on 17/8/23. */ public class Testv { public static volatile int i = 0 ; public static void main(String args[]){ for ( int i = 0 ;i< 1000 ;i++){ new Thread(){ public void run(){ Thread.yield(); Testv.i++; } }.start(); } System.out.println(Testv.i); } } |
要滿足以上這種需求,我們還必須賦予代碼原子性,最常用的肯定是鎖操作了,一個(gè)字穩(wěn),性能可觀,同時(shí)保證原子性和可見性。如果想操作一波的話,還可以考慮使用一些無鎖操作,如CAS,象java.util.concurrent包下的一些原子類就是利用了CAS來做到原子性,但原子性并不能保證可見性,這個(gè)時(shí)候,還需要配合volatile。
以上種種都是對(duì)volatile使用場(chǎng)景的概括,想了解具體的使用場(chǎng)景可以參考博文:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
四、volatile可見性的證明
先上段代碼好了,不知道從何說起了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package com.example.demo.netty; /** * Created with IntelliJ IDEA. * User: chenqimiao * Date: 2017/8/23 * Time: 9:16 * To change this template use File | Settings | File Templates. */ public class VolatileTest { boolean isStop = false ; public void test(){ Thread t1 = new Thread(){ public void run() { isStop= true ; } }; Thread t2 = new Thread(){ public void run() { while (!isStop); } }; t2.start(); t1.start(); } public static void main(String args[]) throws InterruptedException { for ( int i = 0 ;i< 25 ;i++){ new VolatileTest().test(); } } } |
上面這段代碼可能永遠(yuǎn)也不會(huì)結(jié)束,因?yàn)榫€程一對(duì)isStop的賦值,線程二可能對(duì)此并不可見。當(dāng)然只是可能,所以為了放大可見性問題,我這里作了25次循環(huán)。只要有一組線程,“線程一對(duì)isStop的賦值,線程二對(duì)此不可見”的情況發(fā)生,就不會(huì)退出程序。
now,假如你給 isStop 添加一個(gè) volatile 關(guān)鍵字,那么你會(huì)發(fā)現(xiàn)程序立馬就會(huì)退出。
總結(jié)
以上所述是小編給大家介紹的volatile可見性的一些認(rèn)識(shí)和論證,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)服務(wù)器之家網(wǎng)站的支持!
原文鏈接:http://www.cnblogs.com/think-in-java/p/7420979.html