可見(jiàn)性:
一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到。
共享變量:
如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這幾個(gè)線程的共享變量。
Java內(nèi)存模型(JMM)
Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則净捅,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié)。
這里的變量指的是:共享變量
1辩块、所有的變量都存儲(chǔ)在主內(nèi)存中
2蛔六、每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
X 變量就是共享變量:
可見(jiàn)性的兩條規(guī)定:
1废亭、線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行国章,不能直接從主內(nèi)存中讀寫(xiě)
2、不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量豆村,線程間變量值的傳遞需要通過(guò)主內(nèi)存來(lái)完成液兽。
共享變量可見(jiàn)性實(shí)現(xiàn)的原理
線程1對(duì)共享變量的修改要想被線程2及時(shí)看到,必須要經(jīng)過(guò)如下2個(gè)步驟:
1掌动、把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中
2四啰、將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中
示例:
1、首先主內(nèi)存和工作內(nèi)存中 X 的值都為0
?
2粗恢、線程1將自己工作內(nèi)存中 X 的值修改為1柑晒,如圖:
?
3、為了將線程1修改的值更新到線程2的工作內(nèi)存中适滓,需要經(jīng)過(guò)兩個(gè)步驟:首先是將線程1的工作內(nèi)存中的值更新到主內(nèi)存當(dāng)中敦迄。
?
4恋追、最后將主內(nèi)存中 X 的值更新到線程2的工作內(nèi)存當(dāng)中凭迹。這就可以說(shuō)線程1對(duì) X 的修改被線程2及時(shí)的看到了。
?
要實(shí)現(xiàn)共享變量的可見(jiàn)性苦囱,必須保證兩點(diǎn):
1嗅绸、線程修改后的共享變量值能夠及時(shí)從工作內(nèi)存刷新到主內(nèi)存中
2、其他線程能夠及時(shí)把共享變量的最新值從主內(nèi)存更新到自己的工作內(nèi)存中
?
可見(jiàn)性的實(shí)現(xiàn)方式
Java語(yǔ)言層面支持的可見(jiàn)性實(shí)現(xiàn)方式:
synchronized
volatile
?
synchronized實(shí)現(xiàn)可見(jiàn)性
synchronized能夠?qū)崿F(xiàn):
- 原子性(同步)
- 可見(jiàn)性
?
JMM關(guān)于synchronized的兩條規(guī)定:
1撕彤、線程解鎖前鱼鸠,必須把共享變量的最新值刷新到主內(nèi)存中
2、線程加鎖時(shí)羹铅,將清空工作內(nèi)存中共享變量的值蚀狰,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)
從而就實(shí)現(xiàn)了線程解鎖前對(duì)共享變量的修改在下次加鎖時(shí)對(duì)其他線程可見(jiàn)
?
線程執(zhí)行互斥代碼的過(guò)程:
1.獲得互斥鎖
2.清空工作內(nèi)存
3.從主內(nèi)存拷貝變量的最新副本到工作內(nèi)存
4.執(zhí)行代碼
5.將更改后的共享變量的值刷新到主內(nèi)存
6.釋放互斥鎖
?
重排序
重排序:代碼書(shū)寫(xiě)的順序與實(shí)際執(zhí)行的順序不同,指令重排序是編譯器或處理器為了提高程序性能而做的優(yōu)化
1.編譯器優(yōu)化的重排序(編譯器優(yōu)化)
在單線程中职员,保證執(zhí)行結(jié)果正確的情況下麻蹋,改變代碼的執(zhí)行順序。
2.指令級(jí)并行重排序(處理器優(yōu)化)
3.內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)
指的是對(duì)讀寫(xiě)緩存進(jìn)行的優(yōu)化
示例:重排序就是有可能出現(xiàn)下面的情況:
?
as-if-serial 語(yǔ)義
as-if-serial:無(wú)論如何重排序焊切,程序執(zhí)行的結(jié)果應(yīng)該與代碼順序執(zhí)行的結(jié)果一致(Java編譯器扮授、運(yùn)行時(shí)和處理器都會(huì)保證Java在單線程下遵循as-if-serial語(yǔ)義)
?
單線程:第1芳室、2行的順序可以重排,但第3行不能
重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題
但多線程中程序交錯(cuò)執(zhí)行時(shí)刹勃,重排序可能會(huì)造成內(nèi)存可見(jiàn)性問(wèn)題
可見(jiàn)性分析(多線程的情況):
?
執(zhí)行順序:
1.1->2.1->2.2—>1.2(線程的交叉執(zhí)行)
result的值為:3
1.2->2.1->2.2—>1.1(重排序結(jié)合線程交叉執(zhí)行)
result的值為:0
2.1和2.2之間也可以進(jìn)行重排序堪侯,因?yàn)橹灰皇菙?shù)據(jù)相互依存,都是不影響操作流程進(jìn)行重排序的荔仁。如圖:
?
導(dǎo)致共享變量在線程間不可見(jiàn)的原因:
1.線程的交叉執(zhí)行
2.重排序結(jié)合線程交叉執(zhí)行
3.共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新
安全的代碼:(添加了sychronized)
?
synchronized解決方案:
1.線程的交叉執(zhí)行 ——> 原子性伍宦,獲得鎖對(duì)象,每次只能讓一個(gè)線程執(zhí)行鎖里面的代碼乏梁。
2.重排序結(jié)合線程交叉執(zhí)行 ——> 原子性雹拄。因?yàn)楂@得了鎖對(duì)象,并且在單線程中掌呜,無(wú)論如何進(jìn)行重排序滓玖,都不會(huì)影響最后的執(zhí)行結(jié)果。
3.共享變量未及時(shí)更新 ——> 可見(jiàn)性 通過(guò)synchronized
的可見(jiàn)性規(guī)范的規(guī)定來(lái)保證的:1质蕉、加鎖前势篡,清空工作內(nèi)存中的共享變量,與主內(nèi)存中的共享變量進(jìn)行同步模暗。2禁悠、釋放鎖前:將工作內(nèi)存中的共享變量刷新到主內(nèi)存當(dāng)中。
上面的代碼通過(guò)synchronized
以及它的兩條規(guī)范可以保證寫(xiě)操作和讀操作或按照預(yù)期情況進(jìn)行執(zhí)行兑宇,保證共享變量的可見(jiàn)性碍侦。
但反過(guò)來(lái)說(shuō),不加synchronized
也并不是一定會(huì)導(dǎo)致共享變量不可見(jiàn)隶糕。
因?yàn)椴患?code>synchronized瓷产,共享變量也可能可以在工作內(nèi)存和主內(nèi)存中進(jìn)行同步。
這是因?yàn)榫幾g器做了一些優(yōu)化枚驻。它會(huì)去揣摩程序的意圖濒旦,試圖去給出一個(gè)正確的結(jié)果。因此可能程序運(yùn)行很多次才會(huì)出現(xiàn)一次共享變量在工作內(nèi)存和主內(nèi)存中更新不及時(shí)的情況再登,但可能就是因?yàn)檫@樣一次的情況尔邓,導(dǎo)致一些嚴(yán)重的后果。
?
volatile實(shí)現(xiàn)可見(jiàn)性
volatile關(guān)鍵字:
能夠保證volatile變量的可見(jiàn)性
不能保證volatile變量復(fù)合操作的原子性
volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性:
深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的。
- 對(duì)volatile變量執(zhí)行寫(xiě)操作時(shí),會(huì)在寫(xiě)操作后加入一條store屏障指令
??強(qiáng)制的將寫(xiě)緩存中的內(nèi)容強(qiáng)制刷新到主內(nèi)存當(dāng)中瓤球。并且還能防止將volatile之前的變量重排序到volatile寫(xiě)操作之后曙博。
- 對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在讀操作前加入一條load屏障指令
??強(qiáng)制使緩沖區(qū)的緩存失效,使得讀取volatile變量的時(shí)候,必須從主內(nèi)存中進(jìn)行讀取苹粟,同樣也能起到禁止指令重拍序的效果显晶。
通俗地講:volatile變量在每次被線程訪問(wèn)時(shí)贷岸,都強(qiáng)迫從主內(nèi)存中重讀該變量的值。
而當(dāng)該變量發(fā)生變化時(shí)磷雇,又會(huì)強(qiáng)迫線程將最新的值刷新到主內(nèi)存偿警。這樣任何時(shí)刻,不同的線程總能看到該變量的最新值唯笙。
?
線程寫(xiě)volatile變量的過(guò)程:
1.改變線程工作內(nèi)存中volatile變量副本的值
2.將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存
?
線程讀volatile變量的過(guò)程:
1.從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
2.從工作內(nèi)存中讀取volatile變量的副本
?
volatile不能保證volatile變量復(fù)合操作的原子性:
private int number=0螟蒸;
number++;
上面的操作不是原子操作崩掘,因?yàn)?code>number++可以分為三個(gè)步驟:
1.讀取number的值
2.將number的值加1
3.寫(xiě)入最新的number的值
?
加入synchronized七嫌,變?yōu)樵硬僮?/h5>
synchronized(this){
number++;
synchronized(this){
number++;
這是因?yàn)?code>synchronized會(huì)讓number++
的分解出來(lái)的三個(gè)步驟被一個(gè)線程執(zhí)行完成以后苞慢,才會(huì)被另外一個(gè)線程去執(zhí)行诵原。而不會(huì)產(chǎn)生有幾個(gè)線程同時(shí)交叉去執(zhí)行這個(gè)三個(gè)步驟。
而把number
變?yōu)関olatile變量挽放,無(wú)法保證原子性
private volatile int number=0绍赛;
假設(shè)此時(shí)number=5,線程 A 讀取number的值辑畦,當(dāng)線程 A 讀取完畢以后吗蚌,它的 CPU 使用權(quán)被剝奪了,從而導(dǎo)致 A 線程在讀取 number 的過(guò)程被阻塞在這里了纯出。
當(dāng) A 線程被阻塞的時(shí)候蚯妇,B 線程得到了執(zhí)行的機(jī)會(huì),它從主內(nèi)存中讀取 number 的值暂筝,并且對(duì) numebr 的值進(jìn)行加1箩言,并且將改變的值刷新到主內(nèi)存當(dāng)中。此時(shí) B 線程的工作內(nèi)存和主內(nèi)存中 number 的值均為 6乖杠。
而線程 A 的工作內(nèi)存中 number 的值還是5分扎,然后它也在經(jīng)過(guò):對(duì) numebr 的值進(jìn)行加1澄成,并且將改變的值刷新到主內(nèi)存當(dāng)中的操作胧洒。
此時(shí)經(jīng)過(guò)兩次 number++ 的操作,但線程 A 和線程 B 還有主內(nèi)存中 number 的值還是為 6墨状,從而導(dǎo)致實(shí)際輸出與預(yù)想輸出不一致的問(wèn)題卫漫。
這個(gè)問(wèn)題產(chǎn)生的原因是因?yàn)?volatile 不能保證操作的原子性。
解決方案:保證 number 自增操作的原子性:
使用synchronized關(guān)鍵字
使用ReentrantLock
(java.until.concurrent.locks包下)使用AtomicInterger
(vava.util.concurrent.atomic包下)
volatile適用場(chǎng)合
要在多線程中安全的使用volatile變量肾砂,必須同時(shí)滿(mǎn)足:
1.對(duì)變量的寫(xiě)入操作不依賴(lài)其當(dāng)前值
?不滿(mǎn)足:number++列赎、count=count*5等
?滿(mǎn)足:boolean變量、記錄溫度變化的變量等
2.該變量沒(méi)有包含在具有其他變量的不變式中
?不滿(mǎn)足:不變式 low
?
synchronized和volatile比較·
volatile不需要加鎖镐确,比synchronized更輕量級(jí)包吝,不會(huì)阻塞線程饼煞,volatile 執(zhí)行效率會(huì)更高。
從內(nèi)存可見(jiàn)性角度將诗越,volatile讀相當(dāng)于加鎖(會(huì)將工作內(nèi)存和主內(nèi)存中的共享變量進(jìn)行同步)砖瞧,volatile寫(xiě)相當(dāng)于解鎖(會(huì)將工作內(nèi)存中的共享變量同步主內(nèi)存當(dāng)中)。
synchronized既能保證可見(jiàn)性嚷狞,又能保證原子性块促,而volatile只能保證可見(jiàn)性,無(wú)法保證原子性床未。
?
總結(jié)
什么是內(nèi)存可見(jiàn)性
Java內(nèi)存模型(JMM)
實(shí)現(xiàn)可見(jiàn)性的方式:
synchronized 和 volatile
?final也可以保證內(nèi)存可見(jiàn)性竭翠,因?yàn)樽兞勘欢x為 final 以后是不能被修改的
synchronized和volatile實(shí)現(xiàn)內(nèi)存可見(jiàn)性的原理
synchronized實(shí)現(xiàn)可見(jiàn)性
?指令重排序
?as-if-serial語(yǔ)義
- volatile實(shí)現(xiàn)可見(jiàn)性(通過(guò) number++ 來(lái)舉例)
?volatile能夠保證可見(jiàn)性
?volatile不能保證原子性
- 問(wèn):即使沒(méi)有保證可見(jiàn)性的措施,很多時(shí)候共享變量依然能夠在主內(nèi)存和工作內(nèi)存見(jiàn)得到及時(shí)的更新薇搁?
?答:一般只有在短時(shí)間內(nèi)高并發(fā)的情況下(線程之間相互交錯(cuò)執(zhí)行)才會(huì)出現(xiàn)變量得不到及時(shí)更新的情況斋扰,因?yàn)镃PU在執(zhí)行時(shí)會(huì)很快地刷新緩存,所以一般情況下很難看到這種問(wèn)題啃洋。這種情況的出現(xiàn)是隨機(jī)褥实、不可預(yù)測(cè)的。
- 對(duì)64位(long裂允、double)變量的讀寫(xiě)可能不是原子操作:
?Java內(nèi)存模型允許JVM將沒(méi)有被volatile修飾的64位數(shù)據(jù)類(lèi)型的讀寫(xiě)操作劃分為兩次32位的讀寫(xiě)操作來(lái)進(jìn)行
?導(dǎo)致問(wèn)題:有可能會(huì)出現(xiàn)讀取到“半個(gè)變量”的情況
?解決方法:加volatile關(guān)鍵字
- synchronized和volatile比較
?volatile比synchronized更輕量級(jí)
?volatile沒(méi)有synchronized使用的廣泛