詳解java并發(fā)編程(2) --Synchronized與Volatile區(qū)別
1 Synchronized
在多線程并發(fā)中synchronized一直是元老級(jí)別的角色。利用synchronized來實(shí)現(xiàn)同步具體有一下三種表現(xiàn)形式:
- 對(duì)于普通的同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的class對(duì)象。
- 對(duì)于同步方法塊,鎖是synchronized括號(hào)里配置的對(duì)象。
當(dāng)一個(gè)代碼,方法或者類被synchronized修飾以后。當(dāng)一個(gè)線程試圖訪問同步代碼塊的時(shí)候,它首先必須得到鎖,退出或拋出異常的時(shí)候必須釋放鎖。那么這樣做有什么好處呢?
它主要確保多個(gè)線程在同一時(shí)刻,只能有一個(gè)線程處于方法或者同步塊中,它保證了線程對(duì)變量的可見性和排他性。
1.1 如何實(shí)現(xiàn)排他性
如下圖所示,一個(gè)普通的方法會(huì)有一個(gè)左右擺動(dòng)的開關(guān),可以連接到任意一個(gè)線程,如果該方法代碼不是原子性的,可能會(huì)出現(xiàn)一個(gè)線程并沒有將方法代碼執(zhí)行完畢就鏈接到另一個(gè)線程中去。而被synchronized修飾的方法,鏈接到一個(gè)線程后,除非這個(gè)線程將方法執(zhí)行完畢或者拋出異常,開關(guān)才會(huì)鏈接至別的線程。就這樣將一個(gè)并行的操作變了穿行操作。(同一時(shí)間保證只有一個(gè)線程在執(zhí)行方法代碼)
int i = 1; public synchronized void increment(){ i++; }
在前面并發(fā)基礎(chǔ)及鎖的原理中我們介紹過i++并不是原子操作,所有當(dāng)多個(gè)線程同時(shí)操作i++的時(shí)候可能會(huì)出現(xiàn)多線程并發(fā)問題。而上訴代碼塊中i++是在synchronized修飾的方法中。其中一個(gè)線程進(jìn)入該方法首先獲得當(dāng)前實(shí)例對(duì)象的鎖,當(dāng)另一個(gè)線程試圖執(zhí)行該方法的時(shí)候,由于前一個(gè)線程并沒有執(zhí)行完畢釋放掉鎖,所以該線程掛起等待鎖的釋放。
通過加鎖的方式我們實(shí)現(xiàn)了將i++非原子操作的方法變成了原子操作的方法。從而實(shí)現(xiàn)了排他性。
1.2 如何實(shí)現(xiàn)可見性
因?yàn)樵趈ava內(nèi)存模型中規(guī)定:在執(zhí)行被synchronized修飾的代碼時(shí),線程首先獲取鎖→清空工作內(nèi)存→在主內(nèi)存中拷貝最新變量的副本到工作內(nèi)存→執(zhí)行完代碼→將工作內(nèi)存中更改后的共享變量的值刷新到主內(nèi)存中→釋放互斥鎖。
這里有一個(gè)細(xì)節(jié)需要注意: 當(dāng)一個(gè)線程A將最新的共享變量刷新到主內(nèi)存的時(shí)候,會(huì)導(dǎo)致緩存在其他線程B的工作內(nèi)存的這個(gè)共享變量失效。
當(dāng)線程B下一次去訪問這個(gè)變量的時(shí)候,會(huì)發(fā)現(xiàn),工作緩存的這個(gè)變量已經(jīng)失效。會(huì)強(qiáng)制從主內(nèi)存中重新讀取這個(gè)共享變量
2 Volatile
當(dāng)聲明共享變量為volatile后,對(duì)這個(gè)變量的讀/寫將會(huì)很特別。volatile可以說是java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制。他只能能只能保證變量的可見性與讀/寫的原子性。要理解volatile確實(shí)是不容易的,接下來我們進(jìn)入深入的分析!
2.1 volatile的特性
下面有兩個(gè)示例代碼:
public class VolatileTest1 { volatile long a = 0L; //使用volatile聲明64位的long型變量 public void set(long b) { a = b; //單個(gè)volatile變量的寫 } public void increment() { a++; //復(fù)合(多個(gè))volatile變量的讀/寫 } public long get() { return a; //的那個(gè)volatile變量的讀 } }
public class VolatileTest2 { long a = 0L; //64位的普通long型變量 public synchronized void set(long b) { //單個(gè)普通變量的寫使用同步鎖 a = b; } public void increment() { //普通方法調(diào)用 long tmp = get(); //調(diào)用以同步的讀方法 tmp += 1; //普通的寫操作 set(tmp); //調(diào)用以同步的寫方法 } public synchronized long get() { //單個(gè)普通變量的讀使用同步鎖 return a; } }
上述兩個(gè)示例代碼所帶來的的執(zhí)行效果是相同的。
可以看到被volatile修飾的變量讀與寫操作是原子性的。如前面所述,被Synchronized修飾的變量每次寫操作完成后,會(huì)強(qiáng)制將工作內(nèi)存中緩存的共享變量強(qiáng)制刷新到主內(nèi)存中。所以保證了volatile修飾變量的可見性。
從上述示例代碼中我們也能看出,即便讀與寫是原子性,但是依舊不能保證 a++;是原子操作。這也是很多人對(duì)volatile字段理解困難的原因所在。
簡(jiǎn)而言之,volatile變量自身具有下列特征。
- 可見性:對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入。
- 原子性:對(duì)任意單個(gè)volat變量的讀 / 寫具有原子性,但類似volatile++這種復(fù)合操作不具有原子性。
在這里樓主插一個(gè)之前遇到的面試題:請(qǐng)問對(duì)于double和long類型的讀寫是原子性的嗎?double和long類型是64位的,在一些32位的處理器上,可能會(huì)把一個(gè)64位的long/double型變量的寫操作才分為兩個(gè)32位的寫操作來執(zhí)行。座椅此時(shí)對(duì)這個(gè)64位變量的寫操作將不具有原子性。但是如果被volatile修飾的話,寫64位的double和long的操作依舊是原子操作。
2.2 volatile的禁止重排序
除了前面內(nèi)存可見性中講到的volatile關(guān)鍵字可以保證變量修改的可見性之外,還有另一個(gè)重要的作用:在JDK1.5之后,可以使用volatile變量禁止指令重排序。
volatile關(guān)鍵字通過提供“內(nèi)存屏障”的方式來防止指令被重排序,為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能,為此,Java內(nèi)存模型采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
總結(jié)來說:
- volatile寫操作之前的操作不會(huì)被編譯器重排序到寫操作之后。
- volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀操作之前。
- 第一個(gè)是volatile讀操作,第二個(gè)是volatile寫操作,不能重排序
2.3 volatile的使用場(chǎng)景
1.狀態(tài)標(biāo)志
用volatile修飾的boolean 變量來作為while循環(huán)的的判斷條件:當(dāng)這個(gè)變量被其他線程修改的時(shí)候能保證while循環(huán)能立即讀到。
2.一次性安全發(fā)布
初始化對(duì)象的正確步驟為:
- 1、分配對(duì)象的內(nèi)存空間
- 2、初始化對(duì)象
- 3、設(shè)置引用指向剛分配的內(nèi)存地址
然而由于重排序機(jī)制,可能導(dǎo)致2、3步驟重排序,導(dǎo)致初始化對(duì)象的步驟變?yōu)?1-3-2。
著名的雙重檢查鎖定存在的問題就是因?yàn)槌跏蓟瘜?duì)象的重排序,引用所指向的對(duì)象可能還沒有完成初始化,而僅僅是指向了一個(gè)空的內(nèi)存地址。
3.獨(dú)立觀察
這是第一種使用場(chǎng)景的引用。例如一種環(huán)境傳感器能夠感覺環(huán)境溫度。一個(gè)后臺(tái)線程可能會(huì)每隔幾秒讀取一次該傳感器,并更新包含當(dāng)前文檔的 volatile 變量。然后,其他線程可以讀取這個(gè)變量,從而隨時(shí)能夠看到最新的溫度值。
4.開銷較低的讀-寫鎖策略
前面我們介紹過,因?yàn)?++x 實(shí)際上是三種操作(讀、添加、存儲(chǔ))的簡(jiǎn)單組合,如果多個(gè)線程湊巧試圖同時(shí)對(duì) volatile 計(jì)數(shù)器執(zhí)行增量操作,那么它的更新值有可能會(huì)丟失。但是被volatile修飾變量的讀 / 寫卻是原子操作。所以當(dāng)共享變量被volatile修飾之后,我們只需要在復(fù)合操作的方法上加上synchronized比直接用synchronized修飾該變量效率高的多。
2.4 volatile總結(jié)
相對(duì)于synchronized塊的代碼鎖,volatile應(yīng)該是提供了一個(gè)輕量級(jí)的針對(duì)共享變量的鎖,當(dāng)我們?cè)诙鄠€(gè)線程間使用共享變量進(jìn)行通信的時(shí)候需要考慮將共享變量用volatile來修飾。
volatile是一種稍弱的同步機(jī)制,在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,也就不會(huì)執(zhí)行線程阻塞,因此volatilei變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
3 synchronized和volatile的區(qū)別
1、 volatile不會(huì)進(jìn)行加鎖操作:
volatile變量是一種稍弱的同步機(jī)制在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使執(zhí)行線程阻塞,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
2、volatile變量作用類似于同步變量讀寫操作:
從內(nèi)存可見性的角度看,寫入volatile變量相當(dāng)于退出同步代碼塊,而讀取volatile變量相當(dāng)于進(jìn)入同步代碼塊。
3、volatile不如synchronized安全:
在代碼中如果過度依賴volatile變量來控制狀態(tài)的可見性,通常會(huì)比使用鎖的代碼更脆弱,也更難以理解。僅當(dāng)volatile變量能簡(jiǎn)化代碼的實(shí)現(xiàn)以及對(duì)同步策略的驗(yàn)證時(shí),才應(yīng)該使用它。一般來說,用同步機(jī)制會(huì)更安全些。
4、volatile無法同時(shí)保證內(nèi)存可見性和原則性:
加鎖機(jī)制(即同步機(jī)制)既可以確??梢娦杂挚梢源_保原子性,而volatile變量只能確??梢娦?,原因是聲明為volatile的簡(jiǎn)單變量如果當(dāng)前值與該變量以前的值相關(guān),那么volatile關(guān)鍵字不起作用,也就是說如下的表達(dá)式都不是原子操作:“count++”、“count = count+1”。
以上所述是小編給大家介紹的Synchronized與Volatile區(qū)別詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Java依賴-關(guān)聯(lián)-聚合-組合之間區(qū)別_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java依賴-關(guān)聯(lián)-聚合-組合之間區(qū)別理解,依賴關(guān)系比較好區(qū)分,它是耦合度最弱的一種,下文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2017-08-08java?mybatis如何操作postgresql?array數(shù)組類型
這篇文章主要介紹了java?mybatis如何操作postgresql?array數(shù)組類型,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01spring boot輸入數(shù)據(jù)校驗(yàn)(validation)的實(shí)現(xiàn)過程
web項(xiàng)目中,用戶的輸入總是被假定不安全不正確的,在被處理前需要做校驗(yàn)。本文介紹在spring boot項(xiàng)目中實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)的過程,通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-09-09mybatis框架order by作為參數(shù)傳入時(shí)失效的解決
這篇文章主要介紹了mybatis框架order by作為參數(shù)傳入時(shí)失效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06SpringCloud OpenFeign超詳細(xì)講解模板化遠(yuǎn)程通信的實(shí)現(xiàn)
這篇文章主要介紹了SpringCloudSpringboot集成OpenFeign實(shí)現(xiàn)模板化遠(yuǎn)程通信,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2022-07-07