引言
這幾天看了幾篇關(guān)于java的volatile關(guān)鍵字的文章躲庄,今天就想總結(jié)一下關(guān)于volatile的相關(guān)知識(shí)鞏固一下攀唯,雖然工作中很少能遇到volatile關(guān)鍵字忘嫉,但是volatile也是java中很重要的知識(shí)點(diǎn)崭捍。
volatile的官方定義
java語(yǔ)言規(guī)范第三版中對(duì)volatile的定義如下:java編程語(yǔ)言允許線程訪問共享變量尸折,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)確保通過排他鎖單獨(dú)獲得這個(gè)變量殷蛇。java語(yǔ)言提供了volatile实夹,在某些情況下比鎖更加方便橄浓。如果一個(gè)字段被聲明為volatile,java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的亮航。
volatile的特性
從上面的官方定義我們可以看出 volatile實(shí)現(xiàn)了內(nèi)存可見性
內(nèi)存可見性:簡(jiǎn)單的講也就是說一個(gè)線程對(duì) 聲明了volatile的變量進(jìn)行修改荸实,java會(huì)保證其他線程也能看見修改,保證內(nèi)存中的變量是最新的(除volatile外 java中還有synchronized 和 final 能實(shí)現(xiàn) 可見性 這里不做贅述 后面再總結(jié)這個(gè)兩個(gè))
原子性: volatile 能保證單個(gè)volatile的操作是原子性的 但不能保證 形如 i++這樣的操作是原子性的 (下面會(huì)給出代碼)
volatile的使用
pubilc volatile static int i =0;
只要在變量前聲明即可
volatile原子性
volatile 無法保證復(fù)合性操作的原子性
我們可以通過代碼實(shí)驗(yàn)來證明這一點(diǎn)
pubilc class AtomicTest {
volatile static int i =0;
@Override
public void run() {
for(int k = 0 ;k<1000;k++){
i++;
}
}
public static void main(String[] args)throws InterruptedException{
Thread[] threads = new Thread[10];
for (int c = 0;c<10;c++){
threads[c]=new Thread(new AtomicTest());
threads[c].start();
}
for(int c =0;c<10;c++){
threads[c].join();
}
System.out.println(i);
}
}
如上缴淋, 執(zhí)行上述代碼你會(huì)發(fā)現(xiàn)每次輸出的值都會(huì)小于我們所期待的最終值 10000 准给, 也就 i++并不是原子性的。
所以我們并不建議 volatile使用在這種場(chǎng)景下重抖,我們可以用volatile :檢查某個(gè)標(biāo)記以來判斷是否進(jìn)行下一步操作
pubilc class AtomicTest {
volatile boolean flag = false;
pubilc void writer(){
flag = true;
}
pubilc void reader(){
if(flag){
........
}
}
}
volatile的內(nèi)存可見性
volatile的內(nèi)存可見性是通過java內(nèi)存模型對(duì)volatile定義的特殊規(guī)則定義的露氮。在jvm虛擬機(jī)的內(nèi)存模型中 分為本地內(nèi)存和主內(nèi)存,每一個(gè)線程都有自己的本地內(nèi)存仇哆,并且共享同一個(gè)主內(nèi)存
當(dāng)寫一個(gè)volatile變量時(shí)沦辙, java內(nèi)存模型會(huì)把該變量從線程的本地變量刷新到主內(nèi)存
當(dāng)讀一個(gè)volatile變量時(shí),java內(nèi)存模型會(huì)去主內(nèi)存取該變量讹剔,然后將本地內(nèi)存中的值改變
java內(nèi)存模型中的volatile 油讯、
java內(nèi)存模型為了保證volatile 的內(nèi)存可見性 對(duì)volatile還有第二條語(yǔ)義:禁止指令重排序優(yōu)化(關(guān)于重排序后面我也會(huì)整理我的心得分享給大家,簡(jiǎn)單的講就是java會(huì)對(duì)沒有數(shù)據(jù)依賴的操作進(jìn)行指令重排 已達(dá)到提升性能的目的)延欠。
那么為什么要禁止指令重排序呢陌兑?下面我們通過一個(gè)簡(jiǎn)單的代碼來演示
Map mapTest;
char[] configuration;
volatile boolean init =false;
//假設(shè)a線程執(zhí)行writer
public void writer(){
mapTest =new HashMap;
configuration = new char[10];//讀之前的準(zhǔn)工作
init =true;
}
//假設(shè)b線程執(zhí)行該代碼
pubilc void reader(){
if(init ){
.......
}
}
很簡(jiǎn)單的代碼,如果init變量沒有被定義為volatile的由捎,那么 init=true 這段代碼可能由于指令重排序的優(yōu)化兔综,導(dǎo)致其被提前執(zhí)行,這樣會(huì)導(dǎo)致配置b中使用a線程中的配置信息是出錯(cuò)狞玛,
volatile變量的賦值的匯編代碼是這樣的
java代碼 : instance =new singleton()
匯編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
其中 lock addl $0x0,(%esp) 就相當(dāng)于一個(gè)內(nèi)存屏障 對(duì)于volatile變量來說 讀寫會(huì)具有不同的內(nèi)存屏障.
具體會(huì)根據(jù)以下規(guī)則使用不同的內(nèi)存屏障
當(dāng)?shù)诙€(gè)操作是volatile寫時(shí)软驰,不管第一個(gè)操作是什么,都不>>能重排序心肪。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后锭亏。
當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么硬鞍,都不能重排序慧瘤。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
當(dāng)?shù)谝粋€(gè)操作是volatile寫固该,第二個(gè)操作是volatile讀時(shí)锅减,不能重排序。
結(jié)語(yǔ)
引用java并發(fā)編程實(shí)戰(zhàn)中的一段話結(jié)束這篇文章
僅當(dāng)volatile 變量能簡(jiǎn)化代碼的實(shí)習(xí)以及對(duì)同步策略的驗(yàn)證時(shí)伐坏,才應(yīng)該使用它們怔匣,如果在驗(yàn)證正確性時(shí)需要對(duì)可見性進(jìn)行復(fù)雜的判斷,那么就不要使用volatile變量著淆。volatile變量的正確使用方式包括:確保它們自身狀態(tài)的可見性劫狠,確保它們所引用對(duì)象的狀態(tài)的可見性拴疤,以及標(biāo)示一些重要的程序生命周期時(shí)間的發(fā)生(例如,初始化和關(guān)閉)独泞。