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

該系列文章已收錄在公衆號【Ccww技術博客】,原創技術文章早於博客推出

在深刻理解使用Volatile與Synchronized時,應該先理解明白Java內存模型 (Java Memory Model,JMM)java


Java內存模型(Java Memory Model,JMM)

Java內存(JMM)模型是在硬件內存模型基礎上更高層的抽象,它屏蔽了各類硬件和操做系統對內存訪問的差別性,從而實現讓Java程序在各類平臺下都能達到一致的併發效果。安全

JMM的內部工做機制多線程

image.png

  • 主內存:存儲共享的變量值(實例變量和類變量,不包含局部變量,由於局部變量是線程私有的,所以不存在競爭問題)
  • 工做內存:CPU中每一個線程中保留共享變量的副本,線程的工做內存,線程在變動修改共享變量後同步回主內存,在變量被讀取前從主內存刷新變量值來實現的。
  • 內存間的交互操做:不一樣線程之間不能直接訪問不屬於本身工做內存中的變量,線程間變量的值的傳遞須要經過主內存中轉來完成。(lock,unlock,read,load,use,assign,store,write)

JMM內部會有指令重排,而且會有af-if-serial跟happen-before的理念來保證指令的正確性併發

  • 爲了提升性能,編譯器和處理器經常會對既定的代碼執行順序進行指令重排序
  • af-if-serial:無論怎麼重排序,單線程下的執行結果不能被改變
  • 先行發生原則(happen-before):先行發生原則有不少,其中程序次序原則,在一個線程內,按照程序書寫的順序執行,書寫在前面的操做先行發生於書寫在後面的操做,準確地講是控制流順序而不是代碼順序

Java內存模型爲了解決多線程環境下共享變量的一致性問題,包含三大特性,app

  • 原子性:操做一旦開始就會一直運行到底,中間不會被其它線程打斷(這操做能夠是一個操做,也能夠是多個操做),在內存中原子性操做包括read、load、user、assign、store、write,若是須要一個更大範圍的原子性可使用synchronized來實現,synchronized塊之間的操做。
  • 可見性:一個線程修改了共享變量的值,其它線程能當即感知到這種變化,修改以後當即同步回主內存,每次讀取前當即從主內存刷新,可使用volatile保證可見性,也可使用關鍵字synchronized和final。
  • 有序性:在本線程中全部的操做都是有序的;在另外一個線程中,看來全部的操做都是無序的,就可須要使用具備自然有序性的volatile保持有序性,由於其禁止重排序。

在理解了JMM的時,來說講Volatile與Synchronized的使用,Volatile與Synchronized到底有什麼做用呢?jvm


Volatile

Volatile 的特性性能

  • 保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。(實現可見性)
  • 禁止進行指令重排序。(實現有序性)
  • volatile 只能保證對單次讀/寫的原子性,i++ 這種操做不能保證原子性

Volatile可見性

當寫一個volatile變量時,JMM會把該線程對應的工做內存中的共享變量值更新後刷新到主內存,優化

當讀取一個volatile變量時,JMM會把該線程對應的工做內存置爲無效,線程會從主內存中讀取共享變量。this

寫操做:
spa

讀操做:

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)即是括號括起來的對象實例;

更加詳細的解析看[Java併發之Synchronized]()

理解了Volatile與Synchronized後,那咱們來看看如何使用Volatile與Synchronized優化單例模式


單例模式優化-雙重檢測DCL(Double Check Lock)

先來看看通常模式的單例模式:

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}

    public static Singleton getInstance(){
            if(singleton == null){
                singleton = new Singleton();   // 建立實例
        }
        return singleton;
    }

}

可能出現問題:當有兩個線程A和B,

  • 線程A判斷if(singleton == null)準備執行建立實例時,線程掛起,
  • 此時線程B也會判斷singleton爲空,接着執行建立實例對象返回;
  • 最後,因爲線程A已進入也會建立了實例對象,這就致使多個單例對象的狀況

首先想到是那就在使用synchronized做用在靜態方法:

public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(singleton == null){
                singleton = new Singleton();
        }
        return singleton;
    }
}

雖然這樣簡單粗暴解決,但會致使這個方法比較效率低效,致使程序性能嚴重降低,那是否是還有其餘更優的解決方案呢?

能夠進一步優化建立了實例以後,線程再同步鎖以前檢驗singleton非空就會直接返回對象引用,而不用每次都在同步代碼塊中進行非空驗證,

若是隻有synchronized前加一個singleton非空,就會出現第一種狀況多個線程同時執行到條件判斷語句時,會建立多個實例

所以須要在synchronized後加一個singleton非空,就不會出現會建立多個實例,

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized(Singleton.class){
                if(singleton == null)
                    singleton = new Singleton();   
            }
        }
        return singleton;
    }
}

這個優化方案雖然解決了只建立單個實例,因爲存在着指令重排,會致使在多線程下也是不安全的(當發生了重排後,後續的線程發現singleton不是null而直接使用的時候,就會出現意料以外的問題。)。致使緣由singleton = new Singleton()新建對象會經歷三個步驟:

  • 1.內存分配
  • 2.初始化
  • 3.返回對象引用

因爲重排序的緣故,步驟二、3可能會發生重排序,其過程以下:

  • 1.分配內存空間
  • 2.將內存空間的地址賦值給對應的引用
  • 3.初始化對象

那麼問題找到了,那怎麼去解決呢?那就禁止不容許初始化階段步驟2 、3發生重排序,恰好Volatile 禁止指令重排,從而使得雙重檢測真正發揮做用。

public class Singleton {
    //經過volatile關鍵字來確保安全
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){
           synchronized (Singleton.class){
                if(singleton == null){
                singleton = new Singleton();
            }
        }
    }
    return singleton;
    }
}

最終咱們這個完美的雙重檢測單例模式出來了


總結

  • volatile本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。
  • volatile僅能使用在變量級別;synchronized則可使用在變量、方法、和類級別的
  • volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量的修改可見性和原子性
  • volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化
  • 使用volatile而不是synchronized的惟一安全的狀況是類中只有一個可變的域
各位看官還能夠嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支持!
歡迎關注公衆號【Ccww技術博客】,原創技術文章第一時間推出
相關文章
相關標籤/搜索