淺談synchronized和volatitle實(shí)現(xiàn)線(xiàn)程安全的策略
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
作者 | Virtuals
來(lái)源 | urlify.cn/JzmqQ3
什么是線(xiàn)程不安全
我對(duì)線(xiàn)程安全的理解就是多個(gè)線(xiàn)程同時(shí)操作一個(gè)共享變量時(shí)會(huì)產(chǎn)生意料之外的情況,這種情況就是線(xiàn)程不安全。注意:只有寫(xiě)操作才可能出現(xiàn)線(xiàn)程不安全,對(duì)共享變量只進(jìn)行讀操作線(xiàn)程是絕對(duì)安全的。
具體線(xiàn)程不安全的例子有一個(gè)很經(jīng)典的就是兩個(gè)線(xiàn)程都對(duì)一個(gè)共享變量x=0執(zhí)行100次自增操作,但是x的結(jié)果并非200
因此線(xiàn)程不安全的條件是:多線(xiàn)程 + 共享變量 + 寫(xiě)操作
Java的內(nèi)存模型
你可能會(huì)好奇線(xiàn)程是如何獲取共享變量的,這就為你解答
Java線(xiàn)程之間的通信由Java內(nèi)存模型(簡(jiǎn)稱(chēng)JMM)控制,從抽象的角度來(lái)說(shuō),JMM定義了線(xiàn)程和主內(nèi)存之間的抽象關(guān)系。JMM的抽象示意圖如圖所示:

從圖中我們可以看到:
共享變量存在于主內(nèi)存中,也就是堆內(nèi)存
每一個(gè)線(xiàn)程都保存了一份該線(xiàn)程使用到的共享變量的副本
線(xiàn)程讀取共享變量?jī)?yōu)先從本地內(nèi)存(也就是棧內(nèi)存)中讀取,寫(xiě)共享變量先寫(xiě)到棧內(nèi)存,再寫(xiě)入堆內(nèi)存
線(xiàn)程之間對(duì)共享變量的通信只能通過(guò)堆內(nèi)存
以上只是Java內(nèi)存模型的抽象圖,實(shí)際上線(xiàn)程的工作模型是這樣的,棧內(nèi)存即是兩個(gè)緩沖區(qū)

接下來(lái)看一個(gè)線(xiàn)程不安全的例子:
假設(shè)線(xiàn)程A、B操作同一個(gè)共享變量X,初始兩級(jí)Cache都為空
線(xiàn)程A想要讀取X的值,由于兩級(jí)Cache都沒(méi)有命中,因此加載堆內(nèi)存中的X=0,并緩存到兩個(gè)Cache中
線(xiàn)程A修改X的值為1,為為兩個(gè)Cache刷新X,再刷新到堆內(nèi)存
線(xiàn)程B想要獲取X的值,一級(jí)緩存沒(méi)有獲取到,二級(jí)緩存命中,讀取到X=1
線(xiàn)程B想要修改X的值為2,先刷新自,己的一級(jí)緩存為2,再刷新二級(jí)緩存和堆內(nèi)存中的X為2。目前為止一切正常,接下來(lái)重點(diǎn)來(lái)了
線(xiàn)程A想要讀取X的值,一級(jí)緩存命中此時(shí)X=1,但是堆內(nèi)存中的X=2??梢钥吹骄€(xiàn)程B寫(xiě)入的共享變量對(duì)X不可見(jiàn),出現(xiàn)了線(xiàn)程不安全的情況。
由于Java內(nèi)存機(jī)制就是這樣設(shè)計(jì)的,因此多個(gè)線(xiàn)程操作同一個(gè)變量會(huì)產(chǎn)生不安全的問(wèn)題,volatitle關(guān)鍵字這是被設(shè)計(jì)出來(lái)解決這一問(wèn)題的,它只能用于單個(gè)變量。
volatile解決共享變量線(xiàn)程不安全的策略
還是接著上面這個(gè)例子,我們這樣定義X
volatle int X=0
volatile的內(nèi)存語(yǔ)義是:
當(dāng)一個(gè)線(xiàn)程對(duì)volatile修飾的變量進(jìn)行寫(xiě)操作時(shí),JMM會(huì)立即將該線(xiàn)程對(duì)應(yīng)的棧內(nèi)存中的副本的值刷新到堆內(nèi)存中;當(dāng)一個(gè)線(xiàn)程對(duì)volatile修飾的變量進(jìn)行讀時(shí),JMM會(huì)清空此變量的一二級(jí)緩存,直接從堆內(nèi)存中讀取共享變量的值。
volatile可以當(dāng)作一個(gè)輕量級(jí)的鎖來(lái)使用,但volatile僅僅只能保證共享變量?jī)?nèi)存的可見(jiàn)性,不能保證操作共享變量的原子性,而鎖(如synchronized)能保證整段鎖范圍內(nèi)的代碼具有原子性。
synchronized與鎖
首先要明確的是synchronized不是鎖,鎖都是基于對(duì)象的(Object的子類(lèi)),Java中的每一個(gè)對(duì)象都可以作為一個(gè)鎖。
synchronized是Java的一個(gè)關(guān)鍵字,保證臨界區(qū)內(nèi)的代碼同一時(shí)刻只能有一個(gè)線(xiàn)程執(zhí)行。
線(xiàn)程的執(zhí)行代碼在進(jìn)入synchronized代碼塊前會(huì)自動(dòng)獲取內(nèi)部鎖,這時(shí)候其他線(xiàn)程訪(fǎng)問(wèn)該同步代碼塊時(shí)會(huì)被阻塞掛起。拿到內(nèi)部鎖的線(xiàn)程會(huì)在正常退出同步代碼塊或者拋出異常后或者在同步塊內(nèi)調(diào)用了該內(nèi)置鎖資源的wait系列方法時(shí)釋放該內(nèi)置鎖。內(nèi)置鎖是排它鎖,也就是當(dāng)一個(gè)線(xiàn)程獲取這個(gè)鎖后,其他線(xiàn)程必須等待該線(xiàn)程釋放鎖后才能獲取該鎖。
sysnchronized的內(nèi)部鎖可以是:
當(dāng)前類(lèi)的class字節(jié)碼對(duì)象:this.getClass
當(dāng)前類(lèi)的一個(gè)實(shí)例:this
一個(gè)Object對(duì)象
wait和notify方法只能用于synchronized同步代碼塊內(nèi)
synchronized的內(nèi)存語(yǔ)義
與volatile不同
進(jìn)入synchronized塊的內(nèi)存語(yǔ)義是把再synchronized塊內(nèi)使用到的所有共享變量從棧內(nèi)存中清空,這樣就只能從堆內(nèi)存只讀取,保證了內(nèi)存可見(jiàn)性。退出synchronized塊的內(nèi)存語(yǔ)義是把synchronized塊內(nèi)對(duì)共享變量的修改刷新到堆內(nèi)存。
仔細(xì)想想,這其實(shí)也是一個(gè)加鎖和解鎖的過(guò)程,保證共享變量修改的可見(jiàn)性。
總結(jié)
volatile僅能保證單個(gè)共享變量內(nèi)存的可見(jiàn)性,不能保證原子性。而synchronized既可保證同步塊內(nèi)所有共享變量的內(nèi)存可見(jiàn)性,又能保證其操作的原子性。
volatile是一個(gè)輕量級(jí)的保證內(nèi)存可見(jiàn)性的關(guān)鍵字,實(shí)際上并沒(méi)有加鎖。因此它的性能很高。
synchronized是一個(gè)重量級(jí)的鎖,可以用在代碼塊、普通方法以及靜態(tài)方法上。用在代碼塊時(shí)鎖就是synchronized(~)內(nèi)的對(duì)象,用在普通方法時(shí)鎖就是this,用在靜態(tài)方法時(shí)鎖就是this.getClass()
synchronized保證同步塊內(nèi)代碼的原子性,因?yàn)橐M(jìn)行線(xiàn)程上下文切換,性能較低。不過(guò)優(yōu)化過(guò)后性能也還可以。
粉絲福利:Java從入門(mén)到入土學(xué)習(xí)路線(xiàn)圖
??????

??長(zhǎng)按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
