synchronized 與 volatile 原理 —— 內存屏障的重要實踐

單例模式的雙重校驗鎖的實現:html

第一種:java

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

  

在 static 方法上加 synchronized,等同於將整個類鎖住。每當經過此靜態方法獲得該對象時,就須要同步。數組

若是是實例方法(不是 static),那個 synchronized 鎖只會對同一個對象屢次調用該方法纔會同步,不一樣的對象(實例)調用則不保證同步性。緩存

if (_instance == null) {
        _instance = new Singleton();
}

  

第二種:安全

public class Singleton {

    //volatile 防止延遲初始化
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { //判斷是否已有單例生成
            synchronized (Singleton.class) { //獲取單例的方法是static方法,因此鎖的是 .class對象
                if (instance == null)  //判斷 是否在第一重判斷與 synchronized 語句之間的狀態,已經被另外一個 synchronized 塊 賦值
                    instance = new Singleton();//instance爲volatile,如今沒問題了
            }
        }
        return instance;
    }
}

  

判斷是否有單例生成並不須要同步鎖,只有在第一次單例類實例建立時須要同步鎖。而且在同步鎖中賦值時,還需再檢驗一次。併發

 

這裏使用 volatile 的目的是:避免重排序。直接緣由也就是 instance = new Singleton();  初始化(初始化自己是原子操做)一個對象並使另外一個引用指向他 這個過程可分爲多個步驟:異步

  • 1. 分配內存空間,
  • 2. 初始化默認值(區別於構造器方法的初始化),
  • 3. 初始化對象,
  • 4. 將引用與對應的變量綁定。

若是最後 2步替換順序,1243 執行。則致使了可能會出現(怎樣出現?)引用指向了對象並未初始化好的那塊堆內存。post

注意:這裏的 synchronized 不像第一種是直接在整個方法上添加的,而是在內部的代碼塊上添加的,也就是說該方法的第一重判斷是不包括在 synchronized 裏面的,而且返回語句也不在 synchronized 中。當線程一按照1243 的執行順序,首次訪問到 步驟 4時。線程二異步執行到第一重判斷時,它判斷不爲空,獲取到了一個未初始化好的內存。線程一繼續往下執行,它獲取到了一個真實的初始化的對象。ui

 

概念:Jvm 中的 重排序、主存、原子操做this

拓展:

線程安全:多條線程同時工做的狀況下,經過運用線程鎖,原子操做等方法避免多條線程由於同時訪問同一快內存形成的數據錯誤或衝突。

原子性:解決的是某一操做不會被線程調度機制打斷,中間不會有任何context switch (切 換到另外一個線程)。即保證當前爲原子操做。

有序性:解決的是 cpu 進行指令重排序

可見性:解決的是工做內存/寄存器 對主存的不可見

內存屏障:爲了解決寫緩衝器和無效化隊列帶來的有序性和可見性問題,咱們引入了內存屏障。內存屏障是被插入兩個CPU指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障同樣),從而保障有序性的。另外,爲了達到屏障的效果,它也會使處理器寫入、讀取值以前,將寫緩衝器的值寫入高速緩存,清空無效隊列,從而「附帶」的保障了可見性。

 

八 種原子操做:

lock(鎖定):做用於主內存中的變量,它把一個變量標識爲一個線程獨佔的狀態;

unlock(解鎖):做用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定

read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便後面的load動做使用;

load(載入):做用於工做內存中的變量,它把read操做從主內存中獲得的變量值放入工做內存中的變量副本

use(使用):做用於工做內存中的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做;

assign(賦值):做用於工做內存中的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做;

store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送給主內存中以便隨後的write操做使用;

write(操做):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。

 

四種基本內存屏障:
LoadLoad屏障:
對於這樣的語句 Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:
對於這樣的語句 Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
LoadStore屏障:
對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被執行前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:
對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的(沖刷寫緩衝器,清空無效化隊列)。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。

內存屏障分類
  • 按照可見性保障來劃分
    內存屏障可分爲:加載屏障(Load Barrier)和存儲屏障(Store Barrier)。
    加載屏障:StoreLoad屏障可充當加載屏障,做用是使用load 原子操做,刷新處理器緩存,即清空無效化隊列,使處理器在讀取共享變量時,先從主內存或其餘處理器的高速緩存中讀取相應變量,更新到本身的緩存中
    存儲屏障:StoreLoad屏障可充當存儲屏障,做用是使用 store 原子操做,沖刷處理器緩存,即將寫緩衝器內容寫入高速緩存中,使處理器對共享變量的更新寫入高速緩存或者主內存中
    這兩個屏障一塊兒保證了數據在多處理器之間是可見的。

  • 按照有序性保障來劃分
    內存屏障分爲:獲取屏障(Acquire Barrier)和釋放屏障(Release Barrier)。
    獲取屏障:至關於LoadLoad屏障LoadStore屏障的組合。在讀操做後插入,禁止該讀操做與其後的任何讀寫操做發生重排序;
    釋放屏障:至關於LoadStore屏障StoreStore屏障的組合。在一個寫操做以前插入,禁止該寫操做與其前面的任何讀寫操做發生重排序。
    這兩個屏障一塊兒保證了臨界區中的任何讀寫操做不可能被重排序到臨界區以外。

 
 

1. Synchronized 底層原理(保證有序性,可見性,原子性與線程安全)

synchronized編譯成字節碼後,是經過monitorenterlock原子操做抽象而來)和 monitorexitunlock原子操做抽象而來)兩個指令實現的,具體過程以下:

 

能夠發現,synchronized底層經過獲取屏障和釋放屏障的配對使用保證有序性,加載屏障和存儲屏障的配對使用保正可見性。最後又經過鎖的排他性保障了原子性與線程安全。

 

2. Volatile 底層原理(保證有序性,可見性)

與 synchronized 相似,volatile 也是經過內存屏障來保證有序性與可見性,過程以下:

 讀操做:

 

寫操做:

 

通過對比,能夠發現 volatile 少了兩個指令 monitorenter 與 monitorexit 用來保證原子性與線程安全。
 
 
注意:
1、原子性與線程安全的理解
1> 對於基本數據類型的「簡單操做」,除了 long 與 double 外都具備原子性。由於 long 與 double 的讀取和寫入被 Jvm 分離成 2個slot 操做來進行,可使用 volatile 來保證簡單的賦值與返回操做的原子性。這是 volatile 的特殊用法,其基本用法是保證可見性和有序性。
2> 原子類擴展了「簡單操做」的範圍,增長對額外一些操做的原子性。
3> 原子操做並不能確保線程安全,雖然保證部分操做的原子性,可是對於大部分狀況下仍然須要同步控制。
 

2、數組與對象實例中的 volatile

針對的是引用,其含義是對象獲數組的地址具備可見性,可是數組或對象內部的成員改變不具有可見性。這一點跟變量中 final 數組/對象 的用法是相似的,限定是引用地址。

 

關於 synchronized 的知識點

下列說法不正確的是()
A.當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程獲得執行。
B.當一個線程訪問object的一個synchronized(this)同步代碼塊時,另外一個線程仍然能夠訪問該object中的非synchronized(this)同步代碼塊。
C.當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問不會被阻塞。
D.當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就得到了這個object的對象鎖。結果,其它線程對該object對象全部同步代碼部分的訪問都被暫時阻塞。
答案:C,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問將會被阻塞。

 

參考資料


https://blog.csdn.net/guyuealian/article/details/52525724 

https://www.jianshu.com/p/43af2cc32f90

相關文章
相關標籤/搜索