通過前面一章我們了解了synchronized是一個(gè)重量級(jí)的鎖,雖然JVM對(duì)它做了很多優(yōu)化,而下面介紹的volatile則是輕量級(jí)的synchronized映穗。如果一個(gè)變量使用volatile荆烈,則它比使用synchronized的成本更加低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度裸燎。Java語言規(guī)范對(duì)volatile的定義如下:
Java編程語言允許線程訪問共享變量顾瞻,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量德绿。
上面比較繞口荷荤,通俗點(diǎn)講就是說一個(gè)變量如果用volatile修飾了,則Java可以確保所有線程看到這個(gè)變量的值是一致的移稳,如果某個(gè)線程對(duì)volatile修飾的共享變量進(jìn)行更新蕴纳,那么其他線程可以立馬看到這個(gè)更新,這就是所謂的線程可見性个粱。
volatile雖然看起來比較簡(jiǎn)單古毛,使用起來無非就是在一個(gè)變量前面加上volatile即可,但是要用好并不容易(LZ承認(rèn)我至今仍然使用不好都许,在使用時(shí)仍然是模棱兩可)稻薇。
內(nèi)存模型相關(guān)概念
理解volatile其實(shí)還是有點(diǎn)兒難度的,它與Java的內(nèi)存模型有關(guān)梭稚,所以在理解volatile之前我們需要先了解有關(guān)Java內(nèi)存模型的概念颖低,這里只做初步的介紹,后續(xù)LZ會(huì)詳細(xì)介紹Java內(nèi)存模型弧烤。
操作系統(tǒng)語義
計(jì)算機(jī)在運(yùn)行程序時(shí)忱屑,每條指令都是在CPU中執(zhí)行的蹬敲,在執(zhí)行過程中勢(shì)必會(huì)涉及到數(shù)據(jù)的讀寫。我們知道程序運(yùn)行的數(shù)據(jù)是存儲(chǔ)在主存中莺戒,這時(shí)就會(huì)有一個(gè)問題伴嗡,讀寫主存中的數(shù)據(jù)沒有CPU中執(zhí)行指令的速度快,如果任何的交互都需要與主存打交道則會(huì)大大影響效率从铲,所以就有了CPU高速緩存瘪校。CPU高速緩存為某個(gè)CPU獨(dú)有,只與在該CPU運(yùn)行的線程有關(guān)名段。
有了CPU高速緩存雖然解決了效率問題阱扬,但是它會(huì)帶來一個(gè)新的問題:數(shù)據(jù)一致性。在程序運(yùn)行中伸辟,會(huì)將運(yùn)行所需要的數(shù)據(jù)復(fù)制一份到CPU高速緩存中麻惶,在進(jìn)行運(yùn)算時(shí)CPU不再也主存打交道,而是直接從高速緩存中讀寫數(shù)據(jù)信夫,只有當(dāng)運(yùn)行結(jié)束后才會(huì)將數(shù)據(jù)刷新到主存中窃蹋。舉一個(gè)簡(jiǎn)單的例子:
i++
當(dāng)線程運(yùn)行這段代碼時(shí),首先會(huì)從主存中讀取i( i = 1)静稻,然后復(fù)制一份到CPU高速緩存中警没,然后CPU執(zhí)行 + 1 (2)的操作,然后將數(shù)據(jù)(2)寫入到告訴緩存中振湾,最后刷新到主存中杀迹。其實(shí)這樣做在單線程中是沒有問題的,有問題的是在多線程中恰梢。如下:
假如有兩個(gè)線程A佛南、B都執(zhí)行這個(gè)操作(i++),按照我們正常的邏輯思維主存中的i值應(yīng)該=3嵌言,但事實(shí)是這樣么嗅回?分析如下:
兩個(gè)線程從主存中讀取i的值(1)到各自的高速緩存中,然后線程A執(zhí)行+1操作并將結(jié)果寫入高速緩存中摧茴,最后寫入主存中绵载,此時(shí)主存i==2,線程B做同樣的操作,主存中的i仍然=2苛白。所以最終結(jié)果為2并不是3娃豹。這種現(xiàn)象就是緩存一致性問題。
解決緩存一致性方案有兩種:
通過在總線加LOCK#鎖的方式
通過緩存一致性協(xié)議
但是方案1存在一個(gè)問題购裙,它是采用一種獨(dú)占的方式來實(shí)現(xiàn)的懂版,即總線加LOCK#鎖的話,只能有一個(gè)CPU能夠運(yùn)行躏率,其他CPU都得阻塞躯畴,效率較為低下民鼓。
第二種方案,緩存一致性協(xié)議(MESI協(xié)議)它確保每個(gè)緩存中使用的共享變量的副本是一致的蓬抄。其核心思想如下:當(dāng)某個(gè)CPU在寫數(shù)據(jù)時(shí)丰嘉,如果發(fā)現(xiàn)操作的變量是共享變量,則會(huì)通知其他CPU告知該變量的緩存行是無效的嚷缭,因此其他CPU在讀取該變量時(shí)饮亏,發(fā)現(xiàn)其無效會(huì)重新從主存中加載數(shù)據(jù)。
Java內(nèi)存模型
上面從操作系統(tǒng)層次闡述了如何保證數(shù)據(jù)一致性阅爽,下面我們來看一下Java內(nèi)存模型路幸,稍微研究一下Java內(nèi)存模型為我們提供了哪些保證以及在Java中提供了哪些方法和機(jī)制來讓我們?cè)谶M(jìn)行多線程編程時(shí)能夠保證程序執(zhí)行的正確性。
在并發(fā)編程中我們一般都會(huì)遇到這三個(gè)基本概念:原子性付翁、可見性劝赔、有序性。我們稍微看下volatile
原子性
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷胆敞,要么就都不執(zhí)行。
原子性就像數(shù)據(jù)庫里面的事務(wù)一樣杂伟,他們是一個(gè)團(tuán)隊(duì)移层,同生共死。其實(shí)理解原子性非常簡(jiǎn)單赫粥,我們看下面一個(gè)簡(jiǎn)單的例子即可:
i = 0; ---1
j = i ; ---2
i++; ---3
i = j + 1; ---4
上面四個(gè)操作观话,有哪個(gè)幾個(gè)是原子操作,那幾個(gè)不是越平?如果不是很理解频蛔,可能會(huì)認(rèn)為都是原子性操作,其實(shí)只有1才是原子操作秦叛,其余均不是晦溪。
1---在Java中,對(duì)基本數(shù)據(jù)類型的變量和賦值操作都是原子性操作挣跋;
2---包含了兩個(gè)操作:讀取i三圆,將i值賦值給j
3---包含了三個(gè)操作:讀取i值、i + 1 避咆、將+1結(jié)果賦值給i舟肉;
4---同三一樣
在單線程環(huán)境下我們可以認(rèn)為整個(gè)步驟都是原子性操作,但是在多線程環(huán)境下則不同查库,Java只保證了基本數(shù)據(jù)類型的變量和賦值操作才是原子性的(注:在32位的JDK環(huán)境下路媚,對(duì)64位數(shù)據(jù)的讀取不是原子性操作,如long樊销、double*)整慎。要想在多線程環(huán)境下保證原子性脏款,則可以通過鎖、synchronized來確保院领。
volatile是無法保證復(fù)合操作的原子性
可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)弛矛,一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值比然。
在上面已經(jīng)分析了丈氓,在多線程環(huán)境下,一個(gè)線程對(duì)共享變量的操作對(duì)其他線程是不可見的强法。
Java提供了volatile來保證可見性万俗。
當(dāng)一個(gè)變量被volatile修飾后,表示著線程本地內(nèi)存無效饮怯,當(dāng)一個(gè)線程修改共享變量后他會(huì)立即被更新到主內(nèi)存中闰歪,當(dāng)其他線程讀取共享變量時(shí),它會(huì)直接從主內(nèi)存中讀取蓖墅。
當(dāng)然库倘,synchronize和鎖都可以保證可見性。
有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行论矾。
在Java內(nèi)存模型中教翩,為了效率是允許編譯器和處理器對(duì)指令進(jìn)行重排序,當(dāng)然重排序它不會(huì)影響單線程的運(yùn)行結(jié)果贪壳,但是對(duì)多線程會(huì)有影響饱亿。
Java提供volatile來保證一定的有序性。最著名的例子就是單例模式里面的DCL(雙重檢查鎖)闰靴。這里L(fēng)Z就不再闡述了彪笼。
剖析volatile原理
JMM比較龐大,不是上面一點(diǎn)點(diǎn)就能夠闡述的蚂且。上面簡(jiǎn)單地介紹都是為了volatile做鋪墊的配猫。
volatile可以保證線程可見性且提供了一定的有序性,但是無法保證原子性杏死。在JVM底層volatile是采用“內(nèi)存屏障”來實(shí)現(xiàn)的章姓。
上面那段話,有兩層語義
保證可見性识埋、不保證原子性
禁止指令重排序
第一層語義就不做介紹了凡伊,下面重點(diǎn)介紹指令重排序。
在執(zhí)行程序時(shí)為了提高性能窒舟,編譯器和處理器通常會(huì)對(duì)指令做重排序:
編譯器重排序系忙。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序惠豺;
處理器重排序银还。如果不存在數(shù)據(jù)依賴性风宁,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序;
指令重排序?qū)尉€程沒有什么影響蛹疯,他不會(huì)影響程序的運(yùn)行結(jié)果戒财,但是會(huì)影響多線程的正確性。既然指令重排序會(huì)影響到多線程執(zhí)行的正確性捺弦,那么我們就需要禁止重排序饮寞。那么JVM是如何禁止重排序的呢?這個(gè)問題稍后回答列吼,我們先看另一個(gè)原則happens-before幽崩,happen-before原則保證了程序的“有序性”,它規(guī)定如果兩個(gè)操作的執(zhí)行順序無法從happens-before原則中推到出來寞钥,那么他們就不能保證有序性慌申,可以隨意進(jìn)行重排序。其定義如下:
同一個(gè)線程中的理郑,前面的操作 happen-before 后續(xù)的操作蹄溉。(即單線程內(nèi)按代碼順序執(zhí)行。但是您炉,在不影響在單線程環(huán)境執(zhí)行結(jié)果的前提下类缤,編譯器和處理器可以進(jìn)行重排序,這是合法的邻吭。換句話說,這一是規(guī)則無法保證編譯重排和指令重排)宴霸。
監(jiān)視器上的解鎖操作 happen-before 其后續(xù)的加鎖操作囱晴。(Synchronized 規(guī)則)
對(duì)volatile變量的寫操作 happen-before 后續(xù)的讀操作。(volatile 規(guī)則)
線程的start() 方法 happen-before 該線程所有的后續(xù)操作瓢谢。(線程啟動(dòng)規(guī)則)
線程所有的操作 happen-before 其他線程在該線程上調(diào)用 join 返回成功后的操作畸写。
如果 a happen-before b,b happen-before c氓扛,則a happen-before c(傳遞性)枯芬。
我們著重看第三點(diǎn)volatile規(guī)則:對(duì)volatile變量的寫操作 happen-before 后續(xù)的讀操作。為了實(shí)現(xiàn)volatile內(nèi)存語義采郎,JMM會(huì)重排序千所,其規(guī)則如下:
對(duì)happen-before原則有了稍微的了解,我們?cè)賮砘卮疬@個(gè)問題JVM是如何禁止重排序的蒜埋?
觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn)淫痰,加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令整份。lock前綴指令其實(shí)就相當(dāng)于一個(gè)內(nèi)存屏障待错。內(nèi)存屏障是一組處理指令籽孙,用來實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。volatile的底層就是通過內(nèi)存屏障來實(shí)現(xiàn)的火俄。下圖是完成上述規(guī)則所需要的內(nèi)存屏障:
volatile暫且下分析到這里犯建,JMM體系較為龐大,不是三言兩語能夠說清楚的瓜客,后面會(huì)結(jié)合JMM再一次對(duì)volatile深入分析适瓦。
作者:chenssy
鏈接:http://www.reibang.com/p/fb334c1f35ea
來源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)忆家,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處犹菇。</pre>