溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別

發(fā)布時(shí)間:2020-05-21 11:05:04 來源:網(wǎng)絡(luò) 閱讀:453 作者:架構(gòu)師追風(fēng) 欄目:編程語言

作者 : Hollis

回顧一下兩個(gè)關(guān)鍵字:synchronized和volatile

1、Java語言為了解決并發(fā)編程中存在的原子性、可見性和有序性問題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如synchronized、volatile、final、concurren包等。

2、synchronized通過加鎖的方式,使得其在需要原子性、可見性和有序性這三種特性的時(shí)候都可以作為其中一種解決方案,看起來是“萬能”的。的確,大部分并發(fā)控制操作都能使用synchronized來完成。

3、volatile通過在volatile變量的操作前后插入內(nèi)存屏障的方式,保證了變量在并發(fā)場景下的可見性和有序性。

4、volatile關(guān)鍵字是無法保證原子性的,而synchronized通過monitorenter和monitorexit兩個(gè)指令,可以保證被synchronized修飾的代碼在同一時(shí)間只能被一個(gè)線程訪問,即可保證不會(huì)出現(xiàn)CPU時(shí)間片在多個(gè)線程間切換,即可保證原子性。

那么,我們知道,synchronized和volatile兩個(gè)關(guān)鍵字是Java并發(fā)編程中經(jīng)常用到的兩個(gè)關(guān)鍵字,而且,通過前面的回顧,我們知道synchronized可以保證并發(fā)編程中不會(huì)出現(xiàn)原子性、可見性和有序性問題,而volatile只能保證可見性和有序性,那么,既生synchronized、何生volatile?

接下來,本文就來論述一下,為什么Java中已經(jīng)有了synchronized關(guān)鍵字,還要提供volatile關(guān)鍵字。


synchronized的問題

我們都知道synchronized其實(shí)是一種加鎖機(jī)制,那么既然是鎖,天然就具備以下幾個(gè)缺點(diǎn):

1、有性能損耗

雖然在JDK 1.6中對(duì)synchronized做了很多優(yōu)化,如如適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等,但是他畢竟還是一種鎖。

以上這幾種優(yōu)化,都是盡量想辦法避免對(duì)Monitor進(jìn)行加鎖,但是,并不是所有情況都可以優(yōu)化的,況且就算是經(jīng)過優(yōu)化,優(yōu)化的過程也是有一定的耗時(shí)的。

所以,無論是使用同步方法還是同步代碼塊,在同步操作之前還是要進(jìn)行加鎖,同步操作之后需要進(jìn)行解鎖,這個(gè)加鎖、解鎖的過程是要有性能損耗的。

關(guān)于二者的性能對(duì)比,由于虛擬機(jī)對(duì)鎖實(shí)行的許多消除和優(yōu)化,使得我們很難量化這兩者之間的性能差距,但是我們可以確定的一個(gè)基本原則是:volatile變量的讀操作的性能小號(hào)普通變量幾乎無差別,但是寫操作由于需要插入內(nèi)存屏障所以會(huì)慢一些,即便如此,volatile在大多數(shù)場景下也比鎖的開銷要低。

2、產(chǎn)生阻塞

關(guān)于synchronize的實(shí)現(xiàn)原理,無論是同步方法還是同步代碼塊,無論是ACC_SYNCHRONIZED還是monitorenter、monitorexit都是基于Monitor實(shí)現(xiàn)的。

基于Monitor對(duì)象,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入Entry Set,當(dāng)有一個(gè)線程獲取到對(duì)象的鎖之后,才能進(jìn)行The Owner區(qū)域,其他線程還會(huì)繼續(xù)在Entry Set等待。并且當(dāng)某個(gè)線程調(diào)用了wait方法后,會(huì)釋放鎖并進(jìn)入Wait Set等待。

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別

所以,synchronize實(shí)現(xiàn)的鎖本質(zhì)上是一種阻塞鎖,也就是說多個(gè)線程要排隊(duì)訪問同一個(gè)共享對(duì)象。

而volatile是Java虛擬機(jī)提供的一種輕量級(jí)同步機(jī)制,他是基于內(nèi)存屏障實(shí)現(xiàn)的。說到底,他并不是鎖,所以他不會(huì)有synchronized帶來的阻塞和性能損耗的問題。

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別

volatile的附加功能

除了前面我們提到的volatile比synchronized性能好以外,volatile其實(shí)還有一個(gè)很好的附加功能,那就是禁止指令重排。

我們先來舉一個(gè)例子,看一下如果只使用synchronized而不使用volatile會(huì)發(fā)生什么問題,就拿我們比較熟悉的單例模式來看。

我們通過雙重校驗(yàn)鎖的方式實(shí)現(xiàn)一個(gè)單例,這里不使用volatile關(guān)鍵字:

?1???public?class?Singleton?{??
?2??????private?static?Singleton?singleton;??
?3???????private?Singleton?(){}??
?4???????public?static?Singleton?getSingleton()?{??
?5???????if?(singleton?==?null)?{??
?6???????????synchronized?(Singleton.class)?{??
?7???????????????if?(singleton?==?null)?{??
?8???????????????????singleton?=?new?Singleton();??
?9???????????????}??
?10???????????}??
?11???????}??
?12???????return?singleton;??
?13???????}??
?14???}

以上代碼,我們通過使用synchronized對(duì)Singleton.class進(jìn)行加鎖,可以保證同一時(shí)間只有一個(gè)線程可以執(zhí)行到同步代碼塊中的內(nèi)容,也就是說singleton = new Singleton()這個(gè)操作只會(huì)執(zhí)行一次,這就是實(shí)現(xiàn)了一個(gè)單例。

但是,當(dāng)我們?cè)诖a中使用上述單例對(duì)象的時(shí)候有可能發(fā)生空指針異常。這是一個(gè)比較詭異的情況。

我們假設(shè)Thread1 和 Thread2兩個(gè)線程同時(shí)請(qǐng)求Singleton.getSingleton方法的時(shí)候:

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別


  • Step1 ,Thread1執(zhí)行到第8行,開始進(jìn)行對(duì)象的初始化。

  • Step2 ,Thread2執(zhí)行到第5行,判斷singleton == null。

  • Step3 ,Thread2經(jīng)過判斷發(fā)現(xiàn)singleton != null,所以執(zhí)行第12行,返回singleton。

  • Step4 ,Thread2拿到singleton對(duì)象之后,開始執(zhí)行后續(xù)的操作,比如調(diào)用singleton.call()。

以上過程,看上去并沒有什么問題,但是,其實(shí),在Step4,Thread2在調(diào)用singleton.call()的時(shí)候,是有可能拋出空指針異常的。

之所有會(huì)有NPE拋出,是因?yàn)樵赟tep3,Thread2拿到的singleton對(duì)象并不是一個(gè)完整的對(duì)象。

什么叫做不完整對(duì)象,這個(gè)怎么理解呢?

我們這里來先來看一下,singleton = new Singleton();這行代碼到底做了什么事情,大致過程如下:

  • 1、虛擬機(jī)遇到new指令,到常量池定位到這個(gè)類的符號(hào)引用。

  • 2、檢查符號(hào)引用代表的類是否被加載、解析、初始化過。

  • 3、虛擬機(jī)為對(duì)象分配內(nèi)存。

  • 4、虛擬機(jī)將分配到的內(nèi)存空間都初始化為零值。

  • 5、虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置。

  • 6、執(zhí)行方法,成員變量進(jìn)行初始化。

  • 7、將對(duì)象的引用指向這個(gè)內(nèi)存區(qū)域。

我們把這個(gè)過程簡化一下,簡化成3個(gè)步驟:

  • a、JVM為對(duì)象分配一塊內(nèi)存M

  • b、在內(nèi)存M上為對(duì)象進(jìn)行初始化

  • c、將內(nèi)存M的地址復(fù)制給singleton變量

如下圖:

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別

因?yàn)閷?nèi)存的地址賦值給singleton變量是最后一步,所以Thread1在這一步驟執(zhí)行之前,Thread2在對(duì)singleton==null進(jìn)行判斷一直都是true的,那么他會(huì)一直阻塞,直到Thread1將這一步驟執(zhí)行完。

但是,問題就出在以上過程并不是一個(gè)原子操作,并且編譯器可能會(huì)進(jìn)行重排序,如果以上步驟被重排成:

  • a、JVM為對(duì)象分配一塊內(nèi)存M

  • c、將內(nèi)存的地址復(fù)制給singleton變量

  • b、在內(nèi)存M上為對(duì)象進(jìn)行初始化


如下圖:

4個(gè)點(diǎn)說清楚Java中synchronized和volatile的區(qū)別

這樣的話,Thread1會(huì)先執(zhí)行內(nèi)存分配,在執(zhí)行變量賦值,最后執(zhí)行對(duì)象的初始化,那么,也就是說,在Thread1還沒有為對(duì)象進(jìn)行初始化的時(shí)候,Thread2進(jìn)來判斷singleton==null就可能提前得到一個(gè)false,則會(huì)返回一個(gè)不完整的sigleton對(duì)象,因?yàn)樗€未完成初始化操作。

這種情況一旦發(fā)生,我們拿到了一個(gè)不完整的singleton對(duì)象,當(dāng)嘗試使用這個(gè)對(duì)象的時(shí)候就極有可能發(fā)生NPE異常。

那么,怎么解決這個(gè)問題呢?因?yàn)橹噶钪嘏艑?dǎo)致了這個(gè)問題,那就避免指令重排就行了。

所以,volatile就派上用場了,因?yàn)関olatile可以避免指令重排。只要將代碼改成以下代碼,就可以解決這個(gè)問題:

?1???public?class?Singleton?{??
?2??????private?volatile?static?Singleton?singleton;??
?3???????private?Singleton?(){}??
?4???????public?static?Singleton?getSingleton()?{??
?5???????if?(singleton?==?null)?{??
?6???????????synchronized?(Singleton.class)?{??
?7???????????????if?(singleton?==?null)?{??
?8???????????????????singleton?=?new?Singleton();??
?9???????????????}??
?10???????????}??
?11???????}??
?12???????return?singleton;??
?13???????}??
?14???}

對(duì)singleton使用volatile約束,保證他的初始化過程不會(huì)被指令重排。這樣就可以保Thread2 要不然就是拿不到對(duì)象,要不然就是拿到一個(gè)完整的對(duì)象。


synchronized的有序性保證呢?

看到這里可能有朋友會(huì)問了,說到底上面問題是發(fā)生了指令重排,其實(shí)還是個(gè)有序性的問題,不是說synchronized是可以保證有序性的么,這里為什么就不行了呢?

首先,可以明確的一點(diǎn)是:synchronized是無法禁止指令重排和處理器優(yōu)化的。那么他是如何保證的有序性呢?

這就要再把有序性的概念擴(kuò)展一下了。Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是天然有序的。如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。

以上這句話也是《深入理解Java虛擬機(jī)》中的原句,但是怎么理解呢?周志明并沒有詳細(xì)的解釋。這里我簡單擴(kuò)展一下,這其實(shí)和as-if-serial語義有關(guān)。

as-if-serial語義的意思指:不管怎么重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。編譯器和處理器無論如何優(yōu)化,都必須遵守as-if-serial語義。

這里不對(duì)as-if-serial語義詳細(xì)展開了,簡單說就是,as-if-serial語義保證了單線程中,不管指令怎么重排,最終的執(zhí)行結(jié)果是不能被改變的。

那么,我們回到剛剛那個(gè)雙重校驗(yàn)鎖的例子,站在單線程的角度,也就是只看Thread1的話,因?yàn)榫幾g器會(huì)遵守as-if-serial語義,所以這種優(yōu)化不會(huì)有任何問題,對(duì)于這個(gè)線程的執(zhí)行結(jié)果也不會(huì)有任何影響。

但是,Thread1內(nèi)部的指令重排卻對(duì)Thread2產(chǎn)生了影響。

那么,我們可以說,synchronized保證的有序性是多個(gè)線程之間的有序性,即被加鎖的內(nèi)容要按照順序被多個(gè)線程執(zhí)行。但是其內(nèi)部的同步代碼還是會(huì)發(fā)生重排序,只不過由于編譯器和處理器都遵循as-if-serial語義,所以我們可以認(rèn)為這些重排序在單線程內(nèi)部可忽略。


總結(jié)

本文從兩方面論述了volatile的重要性以及不可替代性:

一方面是因?yàn)閟ynchronized是一種鎖機(jī)制,存在阻塞問題和性能問題,而volatile并不是鎖,所以不存在阻塞和性能問題。

另外一方面,因?yàn)?span >volatile借助了內(nèi)存屏障來幫助其解決可見性和有序性問題,而內(nèi)存屏障的使用還為其帶來了一個(gè)禁止指令重排的附件功能,所以在有些場景中是可以避免發(fā)生指令重排的問題的。

所以,在日后需要做并發(fā)控制的時(shí)候,如果不涉及到原子性的問題,可以優(yōu)先考慮使用volatile關(guān)鍵字。


最后

歡迎大家一起交流,喜歡文章記得點(diǎn)個(gè)贊喲,感謝支持!


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI