要想講清楚volatile關(guān)鍵字,這時候就應(yīng)該主動從內(nèi)存模型開始講起,然后說原子性宴咧、可見性、有序性的理解径缅,鋪墊好這些才是到volatile關(guān)鍵字的原理掺栅,假定前面一篇內(nèi)存模型的文章已經(jīng)理解到位了,那么這里直接從volatile的原理開搞纳猪!
首先說結(jié)論氧卧,volatile關(guān)鍵字作用的是保證可見性和有序性,并不保證原子性氏堤。只有synchronized關(guān)鍵字同時保證上述三種特性沙绝。但是volatile是輕量級的synchronized,它在多線程中保證了變量的“可見性”鼠锈∩撩剩可見性的意思是當(dāng)一個線程修改了一個變量的值后,另外的線程能夠讀取到這個變量修改后的值购笆。
1粗悯、保證可見性
用 volatile 關(guān)鍵字修飾的共享變量,編譯成字節(jié)碼后增加 Lock 前綴指令同欠,該指令要做兩件事:
- 將當(dāng)前工作內(nèi)存緩存行的數(shù)據(jù)立即寫回到主內(nèi)存为黎。
- 寫回主內(nèi)存的操作會使其他工作內(nèi)存里緩存了該共享變量地址的數(shù)據(jù)無效(緩存一致性協(xié)議保證的操作)。
對volatile修飾的變量行您,執(zhí)行寫操作的話铭乾,JVM會發(fā)送一條lock前綴指令給CPU,CPU在計算完之后會立即將這個值寫回主內(nèi)存娃循,同時因為有MESI緩存一致性協(xié)議炕檩,所以各個CPU都會對總線進行嗅探,自己本地緩存中的數(shù)據(jù)是否被別人修改捌斧。
如果發(fā)現(xiàn)別人修改了某個緩存的數(shù)據(jù)笛质,那么CPU就會將自己本地緩存的數(shù)據(jù)過期掉,然后這個CPU上執(zhí)行的線程在讀取那個變量的時候捞蚂,就會從主內(nèi)存重新加載最新的數(shù)據(jù)了妇押。
所以,volatile的可見性就是:lock前綴指令 + MESI緩存一致性協(xié)議
2姓迅、保證有序性
Lock 前綴指令有內(nèi)存屏障的作用葡公。上一篇文章已經(jīng)提到過了蒲祈,JMM一共有4種內(nèi)存屏障纵竖,分別是 LoadLoad、LoadStore柴我、StoreStore、StoreLoad扩然。JMM 給volatile插入內(nèi)存屏障保守策略:
- 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障艘儒,后面插入一個 StoreLoad 屏障。
- 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障和一個 LoadStore 屏障夫偶。
StoreStore屏障可以保證在volatile寫之前界睁,前面所有的普通讀寫操作同步到主內(nèi)存中
StoreLoad屏障可以保證防止前面的volatile寫和后面有可能出現(xiàn)的volatile度/寫進行重排序
LoadLoad屏障可以保證防止下面的普通讀操作和上面的volatile讀進行重排序
LoadStore屏障可以保存防止下面的普通寫操作和上面的volatile讀進行重排序
3、不具備原子性
volatile不具備原子性兵拢,多個線程去寫同一個公共volatile修飾的變量會出現(xiàn)線程安全問題翻斟,對于64位的long和double,如果沒有被volatile修飾卵佛,那么對其操作可以不是原子的杨赤。在操作的時候,可以分成兩步截汪,每次對32位操作疾牲。如果使用volatile修飾long和double,那么其讀寫都是原子操作衙解,這里可以稍稍注意下阳柔。
由于volatile不具備原子性,因此還需要有一個原子性能力的補充蚓峦,這里就可以看看這個文章進行回顧下:《CAS是什么舌剂?Atomic包知多少?》
4暑椰、和Synchronized的內(nèi)存屏障差別
按照可見性保障來劃分霍转。內(nèi)存屏障可分為加載屏障(Load Barrier)和存儲屏障(Store Barrier)。加載屏障的作用是刷新處理器緩存一汽,存儲屏障的作用沖刷處理器緩存避消。Java虛擬機會在 MonitorExit ( 釋放鎖 ) 對應(yīng)的機器碼指令之后插入一個存儲屏障,這就保障了寫線程在釋放鎖之前在臨界區(qū)中對共享變量所做的更新對讀線程的執(zhí)行處理器來說是可同步的召夹。相應(yīng)地岩喷,Java 虛擬機會在 MonitorEnter ( 申請鎖 ) 對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個加載屏障,這使得讀線程的執(zhí)行處理器能夠?qū)懢€程對相應(yīng)共享變量所做的更新從其他處理器同步到該處理器的高速緩存中监憎。因此纱意,可見性的保障是通過寫線程和讀線程成對地使用存儲屏障和加載屏障實現(xiàn)的。
按照有序性保障來劃分鲸阔,內(nèi)存屏障可以分為獲取屏障(Acquire Barrier)和釋放屏障 ( Release Barrier )偷霉。獲 取 屏 障 的 使 用 方 式 是 在 一 個 讀 操 作 ( 包括 Read-Modify-Write 以及普通的讀操作 )之后插入該內(nèi)存屏障迄委,其作用是禁止該讀操作與其后的任何讀寫操作之間進行重排序,這相當(dāng)于在進行后續(xù)操作之前先要獲得相應(yīng)共享數(shù)據(jù)的所有權(quán) ( 這也是該屏障的名稱來源 )腾它。釋放屏障的使用方式是在一個寫操作之前插入該內(nèi)存屏障跑筝,其作用是禁止該寫操作與其前面的任何讀寫操作之間進行重排序死讹。這相當(dāng)于在對相應(yīng)共享數(shù)據(jù)操作結(jié)束后釋放所有權(quán)( 這也是該屏障的名稱來源 )瞒滴。 Java虛擬機會在 MonitorEnter( 它包含了讀操作 ) 對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個獲取屏障,并在臨界區(qū)結(jié)束之后 MonitorExit ( 它包含了寫操作 ) 對應(yīng)的機器碼指令之前的地方插入一個釋放屏障赞警。因此妓忍,這兩種屏障就像是三明治的兩層面包片把火腿夾住一樣把臨界區(qū)中的代碼(指令序列)包括起來:
可以發(fā)現(xiàn),與volatile類似愧旦,synchronized底層也是通過釋放屏障和獲取屏障的配對使用保障有序性世剖,加載屏障和存儲屏障的配對使用保障可見性。最后又通過鎖的排他性保障了原子性笤虫。