什么是volatile衙四?
volatile首先這個(gè)詞的音標(biāo):[?v?l?ta?l],翻譯過(guò)來(lái)是易變的侵贵,不穩(wěn)定的意思届搁。
- 它是一個(gè)關(guān)鍵字,翻譯過(guò)來(lái)是易改變的意思窍育。
- 我們經(jīng)常用它來(lái)修飾成員變量卡睦。
- 被它修飾的變量具備兩種特性:第一:保證這個(gè)變量的可見(jiàn)性,但不具有原子性漱抓。第二:禁止進(jìn)行指令重排序表锻。
- 當(dāng)一個(gè)變量被 volatile 修飾時(shí),任何線程對(duì)它的寫(xiě)操作都會(huì)立即刷新到主內(nèi)存中乞娄,并且會(huì)強(qiáng)制讓緩存了該變量的線程中的數(shù)據(jù)清空瞬逊,必須從主內(nèi)存重新讀取最新數(shù)據(jù)。并不是讓線程直接從主內(nèi)存中獲取數(shù)據(jù)仪或,依然需要將變量拷貝到工作內(nèi)存中确镊。
volatile的原理是什么?
- 既然它是關(guān)鍵字范删,那就表明它跟syn一樣蕾域,沒(méi)有可見(jiàn)的源碼,都是jvm層面了到旦,其中涉及到了內(nèi)存模型也就是JMM(Java Memory Model)旨巷。
- 內(nèi)存模型是什么?是Java虛擬機(jī)規(guī)范中定義了一種Java內(nèi)存模型來(lái)屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問(wèn)差異添忘,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果采呐。
- 為了獲得較好的執(zhí)行性能,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來(lái)提升指令執(zhí)行速度搁骑,也沒(méi)有限制編譯器對(duì)指令進(jìn)行重排序斧吐。也就是說(shuō)又固,在java內(nèi)存模型中,也會(huì)存在緩存一致性問(wèn)題和指令重排序的問(wèn)題会通。
- Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類似于物理內(nèi)存)口予,每個(gè)線程都有自己的工作內(nèi)存(類似于高速緩存)。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行涕侈,而不能直接對(duì)主存進(jìn)行操作沪停。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存。也有的人說(shuō)主內(nèi)存可以簡(jiǎn)單認(rèn)為是堆內(nèi)存裳涛,而工作內(nèi)存則可以認(rèn)為是棧內(nèi)存木张。
舉個(gè)例子:
int i = 10;
執(zhí)行線程必須先在自己的工作線程中對(duì)變量i所在的緩存行進(jìn)行賦值操作,然后再寫(xiě)入主存當(dāng)中端三。而不是直接將數(shù)值10寫(xiě)入主存當(dāng)中舷礼。
原子性
即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行郊闯。
一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問(wèn)題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元妻献,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元团赁。如果這2個(gè)操作不具備原子性育拨,會(huì)造成什么樣的后果。假如從賬戶A減去1000元之后欢摄,操作突然中止熬丧。然后又從B取出了500元,取出500元之后怀挠,再執(zhí)行 往賬戶B加上1000元 的操作析蝴。這樣就會(huì)導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒(méi)有收到這個(gè)轉(zhuǎn)過(guò)來(lái)的1000元绿淋。
volatile關(guān)鍵字并不能保證數(shù)據(jù)的原子性闷畸,所以說(shuō)它是線程安全的是錯(cuò)誤的,看似簡(jiǎn)單的 i++ 操作在多線程的環(huán)境下吞滞,是不安全的佑菩,但是可以通過(guò)加鎖來(lái)保證原子性。
i ++ 操作在計(jì)算機(jī)中其實(shí)進(jìn)行了三個(gè)步驟:
- cpu從主內(nèi)存中讀取 i 變量的值冯吓,并且復(fù)制到工作內(nèi)存中倘待。
- 在工作內(nèi)存中+1疮跑。
- 將+1后的結(jié)果刷回主內(nèi)存中组贺。
舉例:
public class VolatileTest implements Runnable {
//使用 volatile 修飾基本數(shù)據(jù)內(nèi)存不能保證原子性
private static volatile int count = 0;
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println(Thread.currentThread().getName() + " : " + count);
}
public static void main(String[] args) {
VolatileTest volatiletest = new VolatileTest();
Thread t1 = new Thread(volatiletest, "t1");
Thread t2 = new Thread(volatiletest, "t2");
t1.start();
t2.start();
}
}
運(yùn)行結(jié)果:幾乎每次都不一樣,這是因?yàn)樽婺铮恿藇olatile 每次都會(huì)去主內(nèi)存中拿最新的值失尖“⊙伲可以通過(guò)給run方法加鎖方式或者利用AtomicInteger來(lái)替換int來(lái)實(shí)現(xiàn)同步。
可見(jiàn)性
可見(jiàn)性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí)掀潮,一個(gè)線程修改了這個(gè)變量的值菇夸,其他線程能夠立即看得到修改的值。
舉例:
// 線程1執(zhí)行的代碼
int i = 0;
i = 10;
// 線程2執(zhí)行的代碼
j = i;
上面的分析可知仪吧,當(dāng)線程1執(zhí)行 i =10這句時(shí)庄新,會(huì)先把i的初始值加載到線程1的高速緩存中,然后賦值為10薯鼠,那么在線程1的高速緩存當(dāng)中i的值變?yōu)?0了择诈,卻沒(méi)有立即寫(xiě)入到主存當(dāng)中。此時(shí)線程2執(zhí)行 j = i出皇,它會(huì)先去主存讀取i的值并加載到線程2的緩存當(dāng)中羞芍,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會(huì)使得j的值為0郊艘,而不是10荷科。這就是可見(jiàn)性問(wèn)題,線程1對(duì)變量i修改了之后纱注,線程2沒(méi)有立即看到線程1修改的值畏浆。
另外,通過(guò)synchronized和Lock也能夠保證可見(jiàn)性奈附,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼全度,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見(jiàn)性斥滤。
有序性和指令重排序(Instruction Reorder)
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行将鸵。(happens-before原則以后再補(bǔ)充。)
舉例:
int i = 0;
boolean flag = false;
i = 1; //語(yǔ)句1
flag = true; //語(yǔ)句2
上面代碼定義了一個(gè)int型變量佑颇,定義了一個(gè)boolean類型變量顶掉,然后分別對(duì)兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看挑胸,語(yǔ)句1是在語(yǔ)句2前面的痒筒,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語(yǔ)句1一定會(huì)在語(yǔ)句2前面執(zhí)行嗎?不一定茬贵,這里可能會(huì)發(fā)生指令重排序簿透。
指令重排序,一般來(lái)說(shuō)解藻,處理器為了提高程序運(yùn)行效率老充,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語(yǔ)句的執(zhí)行先后順序同代碼中的順序一致螟左,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的啡浊。
舉例:
int a = 10; //語(yǔ)句1
int b = 2; //語(yǔ)句2
int c = a + b; //語(yǔ)句3
上面代碼可能發(fā)生的順序?yàn)椋赫Z(yǔ)句2 -->語(yǔ)句1-->語(yǔ)句3觅够,但不可能是語(yǔ)句2 -->語(yǔ)句3-->語(yǔ)句1,因?yàn)樘幚砥髟谶M(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性巷嚣,如果一個(gè)指令2必須用到指令1的結(jié)果喘先,那么處理器會(huì)保證指令1會(huì)在指令2之前執(zhí)行。
但在多線程的情況下:
//線程1:
context = loadContext(); //語(yǔ)句1
inited = true; //語(yǔ)句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中廷粒,由于語(yǔ)句1和語(yǔ)句2沒(méi)有數(shù)據(jù)依賴性窘拯,因此可能會(huì)被重排序。假如發(fā)生了重排序坝茎,在線程1執(zhí)行過(guò)程中先執(zhí)行語(yǔ)句2树枫,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán)景东,去執(zhí)行doSomethingwithconfig(context)方法砂轻,而此時(shí)context并沒(méi)有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)斤吐。
從上面可以看出搔涝,指令重排序不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性和措。也就是說(shuō)庄呈,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性派阱、可見(jiàn)性以及有序性诬留。只要有一個(gè)沒(méi)有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確贫母。
什么場(chǎng)景下會(huì)用到volatile文兑?
- 對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值。
- 該變量沒(méi)有包含在具有其他變量的不變式中腺劣。
- 只有一個(gè)線程寫(xiě)绿贞,多個(gè)線程讀的情況下使用比較頻繁。
哪里用到了volatile橘原?
1. 狀態(tài)標(biāo)記量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
多線程
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2. double check
雙重懶加載的單例模式
這里的 volatile 關(guān)鍵字主要是為了防止指令重排籍铁。
如果不用 ,singleton = new Singleton();趾断,這段代碼其實(shí)是分為三步:
- 分配內(nèi)存空間拒名。
- 初始化對(duì)象。
- 將 singleton 對(duì)象指向分配的內(nèi)存地址芋酌。加上 volatile 是為了讓以上的三步操作順序執(zhí)行增显,反之有可能第二步在第三步之前被執(zhí)行就有可能某個(gè)線程拿到的單例對(duì)象是還沒(méi)有初始化的,以致于報(bào)錯(cuò)隔嫡。
class Singleton {
// 加volatile關(guān)鍵字是防止指令重排甸怕,保證了變量在內(nèi)存中的可見(jiàn)性,但不能保證原子性腮恩。
private volatile static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance==null) {// 此處是為了減少加鎖
synchronized (Singleton.class) {
if(instance==null) {// 此處為了并發(fā)進(jìn)來(lái)后不要重復(fù)new對(duì)象
instance = new Singleton();
}
}
}
return instance;
}
}
volatile能取代synchronized嗎梢杭?
synchronized關(guān)鍵字是防止多個(gè)線程同時(shí)執(zhí)行一段代碼,那么就會(huì)很影響程序執(zhí)行效率秸滴,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized武契,但是volatile關(guān)鍵字是無(wú)法替代synchronized關(guān)鍵字的,因?yàn)関olatile關(guān)鍵字無(wú)法保證操作的原子性荡含。