既生Synchronized,何生Volatile?!
在我的博客和公眾號(hào)中,發(fā)表過(guò)很多篇關(guān)于并發(fā)編程的文章,之前的文章中我們介紹過(guò)了兩個(gè)在Java并發(fā)編程中比較重要的兩個(gè)關(guān)鍵字:synchronized和volatile
我們簡(jiǎn)單回顧一下相關(guān)內(nèi)容:
1、Java語(yǔ)言為了解決并發(fā)編程中存在的原子性、可見(jiàn)性和有序性問(wèn)題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如synchronized、volatile、final、concurren包等。
2、synchronized通過(guò)加鎖的方式,使得其在需要原子性、可見(jiàn)性和有序性這三種特性的時(shí)候都可以作為其中一種解決方案,看起來(lái)是“萬(wàn)能”的。的確,大部分并發(fā)控制操作都能使用synchronized來(lái)完成。
3、volatile通過(guò)在volatile變量的操作前后插入內(nèi)存屏障的方式,保證了變量在并發(fā)場(chǎng)景下的可見(jiàn)性和有序性。
4、volatile關(guān)鍵字是無(wú)法保證原子性的,而synchronized通過(guò)monitorenter和monitorexit兩個(gè)指令,可以保證被synchronized修飾的代碼在同一時(shí)間只能被一個(gè)線程訪問(wèn),即可保證不會(huì)出現(xiàn)CPU時(shí)間片在多個(gè)線程間切換,即可保證原子性。
那么,我們知道,synchronized和volatile兩個(gè)關(guān)鍵字是Java并發(fā)編程中經(jīng)常用到的兩個(gè)關(guān)鍵字,而且,通過(guò)前面的回顧,我們知道synchronized可以保證并發(fā)編程中不會(huì)出現(xiàn)原子性、可見(jiàn)性和有序性問(wèn)題,而volatile只能保證可見(jiàn)性和有序性,那么,既生synchronized、何生volatile?
接下來(lái),本文就來(lái)論述一下,為什么Java中已經(jīng)有了synchronized關(guān)鍵字,還要提供volatile關(guān)鍵字。
1.synchronized的問(wèn)題
我們都知道synchronized其實(shí)是一種加鎖機(jī)制,那么既然是鎖,天然就具備以下幾個(gè)缺點(diǎn):
1、有性能損耗
雖然在JDK 1.6中對(duì)synchronized做了很多優(yōu)化,如如適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等(深入理解多線程(五)—— Java虛擬機(jī)的鎖優(yōu)化技術(shù)),但是他畢竟還是一種鎖。
以上這幾種優(yōu)化,都是盡量想辦法避免對(duì)Monitor(深入理解多線程(四)—— Moniter的實(shí)現(xiàn)原理)進(jìn)行加鎖,但是,并不是所有情況都可以優(yōu)化的,況且就算是經(jīng)過(guò)優(yōu)化,優(yōu)化的過(guò)程也是有一定的耗時(shí)的。
所以,無(wú)論是使用同步方法還是同步代碼塊,在同步操作之前還是要進(jìn)行加鎖,同步操作之后需要進(jìn)行解鎖,這個(gè)加鎖、解鎖的過(guò)程是要有性能損耗的。
關(guān)于二者的性能對(duì)比,由于虛擬機(jī)對(duì)鎖實(shí)行的許多消除和優(yōu)化,使得我們很難量化這兩者之間的性能差距,但是我們可以確定的一個(gè)基本原則是:volatile變量的讀操作的性能小號(hào)普通變量幾乎無(wú)差別,但是寫操作由于需要插入內(nèi)存屏障所以會(huì)慢一些,即便如此,volatile在大多數(shù)場(chǎng)景下也比鎖的開(kāi)銷要低。
2、產(chǎn)生阻塞
我們?cè)谏钊肜斫舛嗑€程(一)——Synchronized的實(shí)現(xiàn)原理中介紹過(guò)關(guān)于synchronize的實(shí)現(xiàn)原理,無(wú)論是同步方法還是同步代碼塊,無(wú)論是ACC_SYNCHRONIZED還是monitorenter、monitorexit都是基于Monitor實(shí)現(xiàn)的。
基于Monitor對(duì)象,當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(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等待。
所以,synchronize實(shí)現(xiàn)的鎖本質(zhì)上是一種阻塞鎖,也就是說(shuō)多個(gè)線程要排隊(duì)訪問(wèn)同一個(gè)共享對(duì)象。
而volatile是Java虛擬機(jī)提供的一種輕量級(jí)同步機(jī)制,他是基于內(nèi)存屏障實(shí)現(xiàn)的。說(shuō)到底,他并不是鎖,所以他不會(huì)有synchronized帶來(lái)的阻塞和性能損耗的問(wèn)題。
2.volatile的附加功能
除了前面我們提到的volatile比synchronized性能好以外,volatile其實(shí)還有一個(gè)很好的附加功能,那就是禁止指令重排。
我們先來(lái)舉一個(gè)例子,看一下如果只使用synchronized而不使用volatile會(huì)發(fā)生什么問(wèn)題,就拿我們比較熟悉的單例模式來(lái)看。
我們通過(guò)雙重校驗(yàn)鎖的方式實(shí)現(xiàn)一個(gè)單例,這里不使用volatile關(guān)鍵字:
- public class Singleton {
- private static Singleton singleton;
- private Singleton (){}
- public static Singleton getSingleton() {
- if (singleton == null) {
- synchronized (Singleton.class) {
- if (singleton == null) {
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
以上代碼,我們通過(guò)使用synchronized對(duì)Singleton.class進(jìn)行加鎖,可以保證同一時(shí)間只有一個(gè)線程可以執(zhí)行到同步代碼塊中的內(nèi)容,也就是說(shuō)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í)候:
- Step1 ,Thread1執(zhí)行到第8行,開(kāi)始進(jìn)行對(duì)象的初始化。
- Step2 ,Thread2執(zhí)行到第5行,判斷singleton == null。
- Step3 ,Thread2經(jīng)過(guò)判斷發(fā)現(xiàn)singleton != null,所以執(zhí)行第12行,返回singleton。
- Step4 ,Thread2拿到singleton對(duì)象之后,開(kāi)始執(zhí)行后續(xù)的操作,比如調(diào)用singleton.call()。
以上過(guò)程,看上去并沒(méi)有什么問(wèn)題,但是,其實(shí),在Step4,Thread2在調(diào)用singleton.call()的時(shí)候,是有可能拋出空指針異常的。
之所有會(huì)有NPE拋出,是因?yàn)樵赟tep3,Thread2拿到的singleton對(duì)象并不是一個(gè)完整的對(duì)象。
什么叫做不完整對(duì)象,這個(gè)怎么理解呢?
我們這里來(lái)先來(lái)看一下,singleton = new Singleton();這行代碼到底做了什么事情,大致過(guò)程如下:
1、虛擬機(jī)遇到new指令,到常量池定位到這個(gè)類的符號(hào)引用。
2、檢查符號(hào)引用代表的類是否被加載、解析、初始化過(guò)。
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è)過(guò)程簡(jiǎn)化一下,簡(jiǎn)化成3個(gè)步驟:
a、JVM為對(duì)象分配一塊內(nèi)存M
b、在內(nèi)存M上為對(duì)象進(jìn)行初始化
c、將內(nèi)存M的地址復(fù)制給singleton變量
如下圖:
因?yàn)閷?nèi)存的地址賦值給singleton變量是最后一步,所以Thread1在這一步驟執(zhí)行之前,Thread2在對(duì)singleton==null進(jìn)行判斷一直都是true的,那么他會(huì)一直阻塞,直到Thread1將這一步驟執(zhí)行完。
但是,問(wèn)題就出在以上過(guò)程并不是一個(gè)原子操作,并且編譯器可能會(huì)進(jìn)行重排序,如果以上步驟被重排成:
- a、JVM為對(duì)象分配一塊內(nèi)存M
- c、將內(nèi)存的地址復(fù)制給singleton變量
- b、在內(nèi)存M上為對(duì)象進(jìn)行初始化
如下圖:
這樣的話,Thread1會(huì)先執(zhí)行內(nèi)存分配,在執(zhí)行變量賦值,最后執(zhí)行對(duì)象的初始化,那么,也就是說(shuō),在Thread1還沒(méi)有為對(duì)象進(jìn)行初始化的時(shí)候,Thread2進(jìn)來(lái)判斷singleton==null就可能提前得到一個(gè)false,則會(huì)返回一個(gè)不完整的sigleton對(duì)象,因?yàn)樗€未完成初始化操作。
這種情況一旦發(fā)生,我們拿到了一個(gè)不完整的singleton對(duì)象,當(dāng)嘗試使用這個(gè)對(duì)象的時(shí)候就極有可能發(fā)生NPE異常。
那么,怎么解決這個(gè)問(wèn)題呢?因?yàn)橹噶钪嘏艑?dǎo)致了這個(gè)問(wèn)題,那就避免指令重排就行了。
所以,volatile就派上用場(chǎng)了,因?yàn)関olatile可以避免指令重排。只要將代碼改成以下代碼,就可以解決這個(gè)問(wèn)題:
- public class Singleton {
- private volatile static Singleton singleton;
- private Singleton (){}
- public static Singleton getSingleton() {
- if (singleton == null) {
- synchronized (Singleton.class) {
- if (singleton == null) {
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
對(duì)singleton使用volatile約束,保證他的初始化過(guò)程不會(huì)被指令重排。這樣就可以保Thread2 要不然就是拿不到對(duì)象,要不然就是拿到一個(gè)完整的對(duì)象。
3.synchronized的有序性保證呢?
看到這里可能有朋友會(huì)問(wèn)了,說(shuō)到底上面問(wèn)題是發(fā)生了指令重排,其實(shí)還是個(gè)有序性的問(wèn)題,不是說(shuō)synchronized是可以保證有序性的么,這里為什么就不行了呢?
首先,可以明確的一點(diǎn)是:synchronized是無(wú)法禁止指令重排和處理器優(yōu)化的。那么他是如何保證的有序性呢?
這就要再把有序性的概念擴(kuò)展一下了。Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是天然有序的。如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無(wú)序的。
以上這句話也是《深入理解Java虛擬機(jī)》中的原句,但是怎么理解呢?周志明并沒(méi)有詳細(xì)的解釋。這里我簡(jiǎn)單擴(kuò)展一下,這其實(shí)和as-if-serial語(yǔ)義有關(guān)。
as-if-serial語(yǔ)義的意思指:不管怎么重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。編譯器和處理器無(wú)論如何優(yōu)化,都必須遵守as-if-serial語(yǔ)義。
這里不對(duì)as-if-serial語(yǔ)義詳細(xì)展開(kāi)了,簡(jiǎn)單說(shuō)就是,as-if-serial語(yǔ)義保證了單線程中,不管指令怎么重排,最終的執(zhí)行結(jié)果是不能被改變的。
那么,我們回到剛剛那個(gè)雙重校驗(yàn)鎖的例子,站在單線程的角度,也就是只看Thread1的話,因?yàn)榫幾g器會(huì)遵守as-if-serial語(yǔ)義,所以這種優(yōu)化不會(huì)有任何問(wèn)題,對(duì)于這個(gè)線程的執(zhí)行結(jié)果也不會(huì)有任何影響。
但是,Thread1內(nèi)部的指令重排卻對(duì)Thread2產(chǎn)生了影響。
那么,我們可以說(shuō),synchronized保證的有序性是多個(gè)線程之間的有序性,即被加鎖的內(nèi)容要按照順序被多個(gè)線程執(zhí)行。但是其內(nèi)部的同步代碼還是會(huì)發(fā)生重排序,只不過(guò)由于編譯器和處理器都遵循as-if-serial語(yǔ)義,所以我們可以認(rèn)為這些重排序在單線程內(nèi)部可忽略。
4.總結(jié)
本文從兩方面論述了volatile的重要性以及不可替代性:
一方面是因?yàn)閟ynchronized是一種鎖機(jī)制,存在阻塞問(wèn)題和性能問(wèn)題,而volatile并不是鎖,所以不存在阻塞和性能問(wèn)題。
另外一方面,因?yàn)関olatile借助了內(nèi)存屏障來(lái)幫助其解決可見(jiàn)性和有序性問(wèn)題,而內(nèi)存屏障的使用還為其帶來(lái)了一個(gè)禁止指令重排的附件功能,所以在有些場(chǎng)景中是可以避免發(fā)生指令重排的問(wèn)題的。
所以,在日后需要做并發(fā)控制的時(shí)候,如果不涉及到原子性的問(wèn)題,可以優(yōu)先考慮使用volatile關(guān)鍵字。
【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號(hào)Hollis(ID:hollischuang)】