面試:爲了進阿里,重新翻閱了Volatile與Synchronized

在深入理解使用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總共有三種用法:

  1. 當synchronized作用在實例方法時,監視器鎖(monitor)便是對象實例(this);

  2. 當synchronized作用在靜態方法時,監視器鎖(monitor)便是對象的Class實例,因爲Class數據存在於永久代,因此靜態方法鎖相當於該類的一個全局鎖;

  3. 當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的唯一安全的情況是類中只有一個可變的域

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章