說(shuō)到volatile侈玄,一些參加過(guò)面試的同學(xué)對(duì)此肯定不陌生。
它是面試官口中的惩患眨客,但是平時(shí)的編碼卻很少打照面(起碼呈昔,我是這樣的)。
最近的面試肝劲,我也經(jīng)常會(huì)問(wèn)到volatile相關(guān)的問(wèn)題辞槐,比如volatile和sychronized的區(qū)別粘室;volatile的使用場(chǎng)景;volatile的實(shí)現(xiàn)原理等等鹿榜。
問(wèn)這些問(wèn)題其實(shí)主要還是考察多線程、鎖等方便的知識(shí)儲(chǔ)備奥裸。雖然volatile在我們?nèi)粘>幋a使用不多沪袭,但是他的實(shí)現(xiàn)思想以及背后牽扯的一些概念還是值得我們學(xué)習(xí)和思考的。
舉例
首先我們來(lái)看一個(gè)例子
<pre style="margin: 0.5em 0px; padding: 0.4em 0.6em; border-radius: 8px; background: rgb(255, 255, 255); color: rgb(0, 0, 0); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; font-family: Menlo; font-size: 9pt;">public class VolatileExample extends Thread {
private static boolean flag = false;
public void run() {
while (!flag) {
}
}
public static void main(String[] args) throws Exception {
new VolatileExample().start();
Thread.sleep(100);
flag = true;
}
}</pre>
這段代碼并不長(zhǎng)侠鳄。
聲明了一個(gè)布爾類型的靜態(tài)變量flag畦攘,初始值為false十电;
main函數(shù)中啟動(dòng)了一個(gè)線程VolatileExample鹃骂,集成自Thread類;
除去VolatileExample線程静盅,當(dāng)然還有一個(gè)主線程main寝殴;
主線程sleep 100毫秒蚣常,并在其后置flag為true抵蚊;
那么,你覺(jué)得運(yùn)行main會(huì)怎樣
順利跑完所有代碼贞绳,結(jié)束冈闭?
程序一直在等待萎攒,不會(huì)結(jié)束臭猜?
我們看下運(yùn)行的結(jié)果
從結(jié)果可以發(fā)現(xiàn)蔑歌,主線程運(yùn)行結(jié)束后次屠,界面中左邊的紅點(diǎn)并沒(méi)有消失劫灶,表示程序還沒(méi)有結(jié)束掖桦。
這是因?yàn)閱?dòng)的VolatileExample線程讀取到的flag一直都是false。
雖然主線程中flag已經(jīng)被賦值為true涌穆,但是VolatileExample線程卻視而不見(jiàn)宿稀,這是為什么呢赖捌,這就涉及到一個(gè)可見(jiàn)性的問(wèn)題越庇。
可見(jiàn)性
這里假設(shè)大家都是對(duì)線程以及鎖等知識(shí)有一定的了解
sychronized我們應(yīng)該都用過(guò)或者了解過(guò),這是為了在多線程環(huán)境下對(duì)共享資源添加一個(gè)標(biāo)識(shí)涩惑,用于鎖住共享資源搬味,防止多線程同時(shí)進(jìn)入對(duì)數(shù)據(jù)操作產(chǎn)生不一致的現(xiàn)象碰纬。
我們平時(shí)說(shuō)的加鎖其實(shí)表面上是為了達(dá)到一種互斥的效果,也就是對(duì)于同時(shí)存在的線程A和線程B悦析,如果這時(shí)候線程A或者鎖强戴,則線程B只能在旁邊乖乖的等骑歹,這就是一種互斥,有你沒(méi)我扁掸,有我沒(méi)你谴分!
但是加鎖的本質(zhì)是解決了可見(jiàn)性的問(wèn)題镀脂。具體先不說(shuō)這個(gè)問(wèn)題,我們先結(jié)合上面的例子來(lái)說(shuō)說(shuō)可見(jiàn)性的問(wèn)題沙兰。后面在結(jié)合可見(jiàn)性可能更好理解加鎖的本質(zhì)僧凰。
在java內(nèi)存模型中训措,是分為主內(nèi)存和工作內(nèi)存兩塊的绩鸣。
主內(nèi)存呀闻,主要是存儲(chǔ)各個(gè)線程都會(huì)用到共享變量等捡多。
對(duì)于每個(gè)線程都有自己的一個(gè)存儲(chǔ)變量的地方,就是工作內(nèi)存垒手。各個(gè)線程之間的工作內(nèi)存是相互獨(dú)立的科贬,不可見(jiàn)的榜掌。
到這里,你可能已經(jīng)知道上面例子的程序?yàn)槭裁匆恢背鲇谶\(yùn)行的狀態(tài)沒(méi)有終止了套硼。沒(méi)錯(cuò),VolatileExample線程讀取的flag值是自己線程中存儲(chǔ)在工作內(nèi)存中的值熟菲,主線程中的flag值雖然更新了抄罕,但是對(duì)于VolatileExample是不可見(jiàn)呆贿、無(wú)感知的做入。
所以竟块,這時(shí)候volatile關(guān)鍵字就排上用場(chǎng)了浪秘。我們?cè)趂lag變量錢添加volatile關(guān)鍵字修飾埠况。再次運(yùn)行程序
效果顯而易見(jiàn)夺衍,主線程和VolatileExample線程一同結(jié)束喜命。
這是因?yàn)槭褂胿olatile關(guān)鍵字修飾矛紫,該變量是強(qiáng)制要求從主內(nèi)存讀以及往主內(nèi)存寫含衔,這樣保證各個(gè)線程在操作這個(gè)變量的值時(shí)二庵,都是最新鮮的數(shù)據(jù)催享。
這時(shí)候杭隙,我們?cè)賮?lái)回顧剛剛說(shuō)到加鎖的本質(zhì)是解決可見(jiàn)性問(wèn)題。
volatile是強(qiáng)制讓所有線程都從一個(gè)地方操作共享資源因妙,使得原來(lái)是從每個(gè)線程的副本中讀取變量變?yōu)閺闹鲀?nèi)存讀取變量痰憎,從而解決了可見(jiàn)性問(wèn)題。
而sychronized也是解決了可見(jiàn)性問(wèn)題攀涵,它不允許同一時(shí)間有兩個(gè)線程操作同一共享資源铣耘,因?yàn)槠錈o(wú)法保證可見(jiàn)性。所以其通過(guò)獨(dú)占互斥的方式以故,保證自己執(zhí)行完之后才會(huì)有下一個(gè)執(zhí)行蜗细,這樣每次只有一個(gè)線程占有資源,也是間接的解決了可見(jiàn)性的問(wèn)題怒详。
說(shuō)到這炉媒,就不得不提另外一個(gè)問(wèn)題——原子性
原子性
private int num = 0;
num++;
上面的代碼符合原子性么?
顯然不符合昆烁,假如現(xiàn)在有線程A和線程B兩個(gè)線程,
線程A讀取num=0蜗元,準(zhǔn)備執(zhí)行num++的操作掌敬,
但是還沒(méi)有執(zhí)行“++”操作之前楷兽,線程B讀取num的值,此時(shí)值為0,之后也執(zhí)行num++的操作,
最后A和B兩個(gè)線程最終得到的值都是1闽晦,
這是不符合原子性的碱蒙。
通過(guò)如下的sychronized的修飾就符合原子性了
sychronized(this) {
num++;
}
其道理還是因?yàn)閟ychronized的獨(dú)占性。
那么volatile是否可以保證原子性呢
答案是否定的。道理和上面沒(méi)有加sychronized的描述是一樣的阶牍。
線程A還有執(zhí)行完num++后磕瓷,線程B也來(lái)訪問(wèn)num值,得到的是0,然后執(zhí)行num++瘩例,最終還是兩個(gè)線程得到的值都是1。
那么volatile有哪些使用場(chǎng)景呢。
volatile的適用場(chǎng)景
volatile是在synchronized性能低下的時(shí)候提出的。如今synchronized的效率已經(jīng)大幅提升,所以volatile存在的意義不大。
如今非volatile的共享變量浩聋,在訪問(wèn)不是超級(jí)頻繁的情況下坊夫,已經(jīng)和volatile修飾的變量有同樣的效果了智听。
volatile不能保證原子性腻惠,這點(diǎn)是大家沒(méi)太搞清楚的复哆,所以很容易出錯(cuò)锈锤。
volatile可以禁止重排序(sychronized也是可以的)。
其實(shí)與之相關(guān)概念還有重排序、happens-before,as-if-serial等等,限于篇幅痰滋,不再詳述。