happen-before原則

一 作用

我們無法就所有場景來規定某個線程修改的變量何時對其他線程可見,但是我們可以指定某些規則,這規則就是happens-before (簡要的說就是,如果A線程滿足happen-before B線程,那麼A線程對變量的操作,都能被B線程觀察到。)

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

二 八大原則

  • 單線程happen-before原則:在同一個線程中,書寫在前面的操作happen-before後面的操作。
  • 鎖的happen-before原則:同一個鎖的unlock操作happen-before此鎖的lock操作。
  • volatile的happen-before原則:對一個volatile變量的寫操作happen-before對此變量的任意操作(當然也包括寫操作了)。
  • happen-before的傳遞性原則:如果A操作 happen-before B操作,B操作happen-before C操作,那麼A操作happen-before C操作。
  • 線程啓動的happen-before原則:同一個線程的start方法happen-before此線程的其它方法。
  • 線程中斷的happen-before原則:對線程interrupt方法的調用happen-before被中斷線程的檢測到中斷髮送的代碼。
  • 線程終結的happen-before原則:線程中的所有操作都happen-before線程的終止檢測。
  • 對象創建的happen-before原則:一個對象的初始化完成先於他的finalize方法調用。

推導出的6條原則:

  1. 將一個元素放入一個線程安全的隊列的操作Happens-Before從隊列中取出這個元素的操作
  2. 將一個元素放入一個線程安全容器的操作Happens-Before從容器中取出這個元素的操作
  3. 在CountDownLatch上的倒數操作Happens-Before CountDownLatch#await()操作
  4. 釋放Semaphore許可的操作Happens-Before獲得許可操作
  5. Future表示的任務的所有操作Happens-Before Future#get()操作
  6. 向Executor提交一個Runnable或Callable的操作Happens-Before任務開始執行操作

如果兩個操作不存在上述(前面8條 + 後面6條)任一一個happens-before規則,那麼這兩個操作就沒有順序的保障,JVM可以對這兩個操作進行重排序。如果操作A happens-before操作B,那麼操作A在內存上所做的操作對操作B都是可見的。

 

三  實例

1.同一個線程中,書寫在前面的操作happen-before後面的操作:

int a = 3;      //1
int b = a + 1; //2

//2 對b賦值的操作會用到變量a,那麼java的“單線程happen-before原則”就保證 //1一定happen-before//2,並且禁止虛擬機對//1 與//2操作進行重排序。

int a = 3;
int b = 4;

兩個語句直接沒有依賴關係,所以指令重排序可能發生,即對b的賦值可能先於對a的賦值。

 

2.同一個鎖的unlock操作happen-beofre此鎖的lock操作

public class A {
   public int var;

   private static A a = new A();

   private A(){}

   public static A getInstance(){
       return a;
   }

   public synchronized void method1(){
       var = 3;
   }

   public synchronized void method2(){
       int b = var;
   }

   public void method3(){
       synchronized(new A()){ //注意這裏和method1 method2 用的可不是同一個鎖哦
           var = 4;
       }
   }
}
//線程1執行的代碼:
A.getInstance().method1(); 
//線程2執行的代碼:
A.getInstance().method2(); 
//線程3執行的代碼:
A.getInstance().method3();

如果某個時刻執行完“線程1” 馬上執行“線程2”,因爲“線程1”執行A類的method1方法後肯定要釋放鎖,“線程2”在執行A類的method2方法前要先拿到鎖,符合“鎖的happen-before原則”,那麼在“線程2”method2方法中的變量var一定是3,所以變量b的值也一定是3。

但是如果是“線程1”、“線程3”、“線程2”這個順序,那麼最後“線程2”method2方法中的b值是3,還是4呢?其結果是可能是3,也可能是4。的確“線程3”在執行完method3方法後的確要unlock,然後“線程2”有個lock,但是這兩個線程用的不是同一個鎖,所以JMM這個兩個操作之間不符合八大happen-before中的任何一條,所以JMM不能保證“線程3”對var變量的修改對“線程2”一定可見,雖然“線程3”先於“線程2”發生。


3.對一個volatile變量的寫操作happen-before對此變量的任意操作

volatile int a;

a = 1; //1

b = a;  //2

如果線程1 執行//1,“線程2”執行了//2,並且“線程1”執行後,“線程2”再執行,那麼符合“volatile的happen-before原則”所以“線程2”中的a值一定是1。


4.如果A操作 happen-before B操作,B操作happen-before C操作,那麼A操作happen-before C操作:

volatile int var;
int b;
int c;
b = 4; //1
var = 3; //2
c = var; //3
c = b; //4

假設“線程1”執行//1 //2這段代碼,“線程2”執行//3 //4這段代碼。如果某次的執行順序如下:
//1 //2 //3 //4。那麼有如下推導( hd(a,b)表示a happen-before b):

因爲有hd(//1,//2) 、hd(//3,//4) (單線程的happen-before原則)
且hd(//2,//3) (volatile的happen-before原則)
所以有 hd(//1,//3),可導出hd(//1,//4) (happen-before原則的傳遞性)
所以變量c的值最後爲4
如果某次的執行順序如下:
//1 //3 //2// //4 那麼最後4的結果就不能確定嘍。其原因是 //3 //2  不符合上述八大原則中的任何一個,不能通過傳遞性推測出來什麼。


5.

private int i = 0;

public void write(int j ){
    i = j;
}

public int read(){
    return i;
}

我們約定線程A執行write(),線程B執行read(),且線程A優先於線程B執行,那麼線程B獲得結果是什麼?我們就這段簡單的代碼一次分析happens-before的規則:

  1. 由於兩個方法是由不同的線程調用,所以肯定不滿足單線程happen-before原則;
  2. 兩個方法都沒有使用鎖,所以不滿足鎖定規則;
  3. 變量i不是用volatile修飾的,所以volatile變量規則不滿足;
  4. 傳遞規則肯定不滿足;

所以我們無法通過happens-before原則推導出線程A happens-before線程B,雖然可以確認在時間上線程A優先於線程B指定,但是就是無法確認線程B獲得的結果是什麼,所以這段代碼不是線程安全 的。那麼怎麼修復這段代碼呢?滿足規則2、3任一即可。

 

 

轉載:

作者:aworker
鏈接:https://www.jianshu.com/p/1508eedba54d
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。


作者: chenssy
出處: http://www.cnblogs.com/chenssy/

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