在深入理解使用Volatile與Synchronized時,應該先理解明白Java內存模型 (Java Memory Model,JMM)
Java內存模型(Java Memory Model,JMM)
Java內存(JMM)模型是在硬件內存模型基礎上更高層的抽象,它屏蔽了各種硬件和操作系統對內存訪問的差異性,從而實現讓Java程序在各種平臺下都能達到一致的併發效果。
JMM的內部工作機制
主內存:存儲共享的變量值(實例變量和類變量,不包含局部變量,因爲局部變量是線程私有的,因此不存在競爭問題)
工作內存:CPU中每個線程中保留共享變量的副本,線程的工作內存,線程在變更修改共享變量後同步回主內存,在變量被讀取前從主內存刷新變量值來實現的。
內存間的交互操作:不同線程之間不能直接訪問不屬於自己工作內存中的變量,線程間變量的值的傳遞需要通過主內存中轉來完成。(lock,unlock,read,load,use,assign,store,write)
JMM內部會有指令重排,並且會有af-if-serial跟happen-before的理念來保證指令的正確性
爲了提高性能,編譯器和處理器常常會對既定的代碼執行順序進行指令重排序
af-if-serial:不管怎麼重排序,單線程下的執行結果不能被改變
先行發生原則(happen-before):先行發生原則有很多,其中程序次序原則,在一個線程內,按照程序書寫的順序執行,書寫在前面的操作先行發生於書寫在後面的操作,準確地講是控制流順序而不是代碼順序
Java內存模型爲了解決多線程環境下共享變量的一致性問題,包含三大特性,
原子性:操作一旦開始就會一直運行到底,中間不會被其它線程打斷(這操作可以是一個操作,也可以是多個操作),在內存中原子性操作包括read、load、user、assign、store、write,如果需要一個更大範圍的原子性可以使用synchronized來實現,synchronized塊之間的操作。
可見性:一個線程修改了共享變量的值,其它線程能立即感知到這種變化,修改之後立即同步回主內存,每次讀取前立即從主內存刷新,可以使用volatile保證可見性,也可以使用關鍵字synchronized和final。
有序性:在本線程中所有的操作都是有序的;在另一個線程中,看來所有的操作都是無序的,就可需要使用具有天然有序性的volatile保持有序性,因爲其禁止重排序。
在理解了JMM的時,來講講Volatile與Synchronized的使用,Volatile與Synchronized到底有什麼作用呢?
Volatile
Volatile 的特性:
保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)
禁止進行指令重排序。(實現有序性)
volatile 只能保證對單次讀/寫的原子性,i++ 這種操作不能保證原子性
Volatile可見性
當寫一個volatile變量時,JMM會把該線程對應的工作內存中的共享變量值更新後刷新到主內存,
當讀取一個volatile變量時,JMM會把該線程對應的工作內存置爲無效,線程會從主內存中讀取共享變量。
寫操作:
讀操作:
Volatile 禁止指令重排
JMM對volatile的禁止指令重排採用內存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。在每個volatile寫操作的後面插入一個StoreLoad屏障
在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個LoadStore屏障
Synchronized
Synchronized是Java中解決併發問題的一種最常用的方法,也是最簡單的一種方法。Synchronized的作用主要有三個:
原子性:確保線程互斥的訪問同步代碼;
可見性:保證共享變量的修改能夠及時可見,其實是通過Java內存模型中的 “對一個變量unlock操作之前,必須要同步到主內存中;如果對一個變量進行lock操作,則將會清空工作內存中此變量的值,在執行引擎使用此變量前,需要重新從主內存中load操作或assign操作初始化變量值” 來保證的
有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”;
Synchronized總共有三種用法:
當synchronized作用在實例方法時,監視器鎖(monitor)便是對象實例(this);
當synchronized作用在靜態方法時,監視器鎖(monitor)便是對象的Class實例,因爲Class數據存在於永久代,因此靜態方法鎖相當於該類的一個全局鎖;
當synchronized作用在某一個對象實例時,監視器鎖(monitor)便是括號括起來的對象實例;
理解了Volatile與Synchronized後,那我們來看看如何使用Volatile與Synchronized優化單例模式
單例模式優化-雙重檢測DCL(Double Check Lock)
先來看看一般模式的單例模式:
class Singleton{
可能出現問題:當有兩個線程A和B,
線程A判斷
if(singleton == null)
準備執行創建實例時,線程掛起,此時線程B也會判斷singleton爲空,接着執行創建實例對象返回;
最後,由於線程A已進入也會創建了實例對象,這就導致多個單例對象的情況
首先想到是那就在使用synchronized作用在靜態方法:
public class Singleton {
雖然這樣簡單粗暴解決,但會導致這個方法比較效率低效,導致程序性能嚴重下降,那是不是還有其他更優的解決方案呢?
可以進一步優化創建了實例之後,線程再同步鎖之前檢驗singleton非空就會直接返回對象引用,而不用每次都在同步代碼塊中進行非空驗證,
如果只有synchronized前加一個singleton非空,就會出現第一種情況多個線程同時執行到條件判斷語句時,會創建多個實例
因此需要在synchronized後加一個singleton非空,就不會出現會創建多個實例,
class Singleton{
這個優化方案雖然解決了只創建單個實例,由於存在着指令重排,會導致在多線程下也是不安全的(當發生了重排後,後續的線程發現singleton不是null而直接使用的時候,就會出現意料之外的問題。)。導致原因 singleton = new Singleton()
新建對象會經歷三個步驟:
1.內存分配
2.初始化
3.返回對象引用
由於重排序的緣故,步驟2、3可能會發生重排序,其過程如下:
1.分配內存空間
2.將內存空間的地址賦值給對應的引用
3.初始化對象
那麼問題找到了,那怎麼去解決呢?那就禁止不允許初始化階段步驟2 、3發生重排序,剛好Volatile 禁止指令重排,從而使得雙重檢測真正發揮作用。
public class Singleton {
最終我們這個完美的雙重檢測單例模式出來了
總結
volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化
使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域