happens-before理解和應用

1.happens-before的理解

1.1 爲何要有一個happens-before的原則?

結論:happens-before以爲着何時變量操做對你可見。php

咱們知道cpu的運行極快,而讀取主存對於cpu而言有點慢了,在讀取主存的過程當中cpu一直閒着(也沒數據能夠運行),這對資源來講形成極大的浪費。因此慢慢的cpu演變成了多級cache結構,cpu在讀cache的速度比讀內存快了n倍。java

當線程在執行時,會保存臨界資源的副本到私有work memory中,這個memory在cache中,修改這個臨界資源會更新work memory但並不必定馬上刷到主存中,那麼何時應該刷到主存中呢?何時和其餘副本同步?
並且編譯器爲了提升指令執行效率,是能夠對指令重排序的,重排序後指令的執行順序不同,有可能線程2讀取某個變量時,線程1還未進行寫入操做。這就是線程可見性的來源。segmentfault

針對以上兩個問題,JMM給出happens-before通用的規則(注意這僅對java而言,其餘的就布吉島了)app

1.2 happens-before原則有啥好處?

i = 1;       //線程A執行
j = i ;      //線程B執行

j 是否等於1呢?假定線程A的操做(i = 1)happens-before線程B的操做(j = i)。
那麼能夠肯定線程B執行後j = 1 必定成立。
若是他們不存在happens-before原則,那麼j = 1 不必定成立。函數

(即便代碼是先執行i=1,而後執行j=i,也不必定j=1,主要看是否符合happens-before)this

1.3 happens-before原則

  1. 若是操做1 happens-before 操做2,那麼第操做1的執行結果將對操做2可見,並且操做1的執行順序排在第操做2以前。
  2. 兩個操做之間存在happens-before關係,並不意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

1.4如何判斷是否爲 happens-before?

  • 程序次序規則: 在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做
    同一個線程中前面的全部寫操做對後面的操做可見
  • 管理鎖定規則:一個unlock操做happen—before後面(時間上的前後順序)對同一個鎖的lock操做。
    若是線程1解鎖了monitor a,接着線程2鎖定了a,那麼,線程1解鎖a以前的寫操做都對線程2可見(線程1和線程2能夠是同一個線程)
  • volatile變量規則:對一個volatile變量的寫操做happen—before後面(時間上)對該變量的讀操做。
    若是線程1寫入了volatile變量v(臨界資源),接着線程2讀取了v,那麼,線程1寫入v及以前的寫操做都對線程2可見(線程1和線程2能夠是同一個線程)
  • 線程啓動規則:Thread.start()方法happen—before調用用start的線程前的每個操做。
    假定線程A在執行過程當中,經過執行ThreadB.start()來啓動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行前對線程B可見。注意:線程B啓動以後,線程A在對變量修改線程B未必可見。
  • 線程終止規則:線程的全部操做都happen—before對此線程的終止檢測,能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
    (線程t1寫入的全部變量,在任意其它線程t2調用t1.join(),或者t1.isAlive() 成功返回後,都對t2可見。)
  • 線程中斷規則:對線程interrupt()的調用 happen—before 發生於被中斷線程的代碼檢測到中斷時事件的發生。
    (線程t1寫入的全部變量,調用Thread.interrupt(),被打斷的線程t2,能夠看到t1的所有操做)
  • 對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
    (對象調用finalize()方法時,對象初始化完成的任意操做,同步到所有主存同步到所有cache。)
  • 傳遞性:若是操做A happen—before操做B,操做B happen—before操做C,那麼能夠得出A happen—before操做C。
    A h-b B , B h-b C 那麼能夠獲得 A h-b C

1.5 一言以蔽之,這些規則背後的道理

在程序運行過程當中,全部的變動會先在寄存器或本地cache中完成,而後纔會被拷貝到主存以跨越內存柵欄(本地或工做內存到主存之間的拷貝動做),此種跨越序列或順序稱爲happens-before。
注:happens-before本質是順序,重點是跨越內存柵欄
一般狀況下,寫操做必需要happens-before讀操做,即寫線程須要在全部讀線程跨越內存柵欄以前完成本身的跨越動做,其所作的變動才能對其餘線程可見。線程

2.應用

2.1 單例模式

單例模式可能存在問題哦,請看個人文章【單例模式】DCL的問題和解決方法code

能夠看出,若是有兩個線程都執行過synchronized ,那麼符合"管理鎖定規則",那麼咱們能夠線程 singleton即便不加上volatile,也不會影響線程間的可見性對象

public class Singleton {     
    private static Singleton singleton;  
    private Singleton() {      }     
    public static Singleton getInstance() {     
        if (singleton == null) {    
            synchronized (Singleton.class) {     
                if (singleton == null) {     
                    Singleton temp = null;  
                    try {  
                        temp = new Singleton();    
                    } catch (Exception e) {   }  
                    if (temp != null) 
                        singleton = temp; 
                }    
            }    
        }    
        return singleton;    
    }  
}

2.2 CopyOnWriteArrayList 的例子

線程A和線程B要執行的如下代碼,最後結果b=1嗎?(_其中list爲CopyOnWriteArrayList_)排序

線程A 線程B
a = 1; list.get(0);
list.set(1,""); int b = a;

執行順序流1:

步驟 線程A 線程B
a a = 1;
b list.set(1,"");
c list.get(0);
d int b = a;

執行順序流2:

步驟 線程A 線程B
a a = 1;
b list.get(0);
c list.set(1,"");
d int b = a;

在確線程B是否必定能看到線程A的a變量前,咱們先看看CopyOnWriteArrayList 的源碼:
能夠發現基本get/set都是一個volatile申明的array變量

private transient volatile Object[] array;

    public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //TODO xxx
            setArray(newElements);
        } finally {
            lock.unlock();
    }
    final void setArray(Object[] a) {
        array = a;
    }

經過咱們的源碼的分析,基本能夠判斷這裏要用到volatile變量規則,
即:對一個volatile變量的寫操做happen—before後面(時間上)對該變量的讀操做。

咱們對執行順序流進行分析:
(步驟a happens-before 步驟b 記爲 hb(a,b))

順序流1:
根據程序次序規則能夠獲得 hb(a,b),hb(c,d),若是咱們但願b=1,那麼只須要 hb(b,c)
因爲volatile變量規則,咱們能夠獲得hb(b,c),因此必定b=1。

順序流2:
根據程序次序規則能夠獲得 hb(a,c),hb(b,d),若是咱們但願b=1,那麼咱們須要hb(a,b)或hb(c,d)。 然而沒有規則能夠獲得以上條件,故不成立,b不必定等於1。

相關文章
相關標籤/搜索