首頁 >Java >java教程 >Java指令重排序的問題解決

Java指令重排序的問題解決

黄舟
黄舟原創
2017-09-28 09:23:472078瀏覽

下面小編就為大家帶來一篇淺談java指令重排序的問題。小編覺得蠻不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

指令重排序是個比較複雜、覺得有些不可思議的問題,同樣是先以例子開頭(建議大家跑下例子,這是實實在在可以重現的,重排序的機率還是挺高的),有個感性的認識


/**
 * 一个简单的展示Happen-Before的例子.
 * 这里有两个共享变量:a和flag,初始值分别为0和false.在ThreadA中先给  a=1,然后flag=true.
 * 如果按照有序的话,那么在ThreadB中如果if(flag)成功的话,则应该a=1,而a=a*1之后a仍然为1,下方的if(a==0)应该永远不会为
 * 真,永远不会打印.
 * 但实际情况是:在试验100次的情况下会出现0次或几次的打印结果,而试验1000次结果更明显,有十几次打印.
 */
public class SimpleHappenBefore {
 /** 这是一个验证结果的变量 */
 private static int a=0;
 /** 这是一个标志位 */
 private static boolean flag=false;
 public static void main(String[] args) throws InterruptedException {
  //由于多线程情况下未必会试出重排序的结论,所以多试一些次
  for(int i=0;i<1000;i++){
   ThreadA threadA=new ThreadA();
   ThreadB threadB=new ThreadB();
   threadA.start();
   threadB.start();
   //这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些.
   threadA.join();
   threadB.join();
   a=0;
   flag=false;
  }
 }
 static class ThreadA extends Thread{
  public void run(){
  a=1;
  flag=true;
  }
 }
 static class ThreadB extends Thread{
  public void run(){
   if(flag){
   a=a*1;
   }
   if(a==0){
   System.out.println("ha,a==0");
   }
  }
 }
}

例子比較簡單,也添加了註釋,不再詳細敘述。

什麼是指令重新排序?有兩個層面:

在虛擬機層面,為了盡可能減少記憶體操作速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規則(這規則後面再敘述)將程式編寫順序打亂-即寫在後面的程式碼在時間順序上可能會先執行,而寫在前面的程式碼會後執行-以盡可能充分地利用CPU。拿上面的例子來說:假如不是a=1的操作,而是a=new byte[1024*1024](分配1M空間)`,那麼它會運行地很慢,此時CPU是等待其執行結束呢,還是先執行下面那句flag=true呢?顯然,先執行flag=true可以提前使用CPU,加快整體效率,當然這樣的前提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裡有兩種情況:後面的程式碼先於前面的程式碼開始執行;前面的程式碼先開始執行,但當效率較慢的時候,後面的程式碼開始執行並先於前面的程式碼執行結束。不管誰先開始,總之後面的程式碼在某些情況下存在先結束的可能。

在硬體層面,CPU會將接收到的一批指令依照其規則重排序,同樣是基於CPU速度比快取速度快的原因,和上一點的目的類似,只是硬體處理的話,每次只能在接收到的有限指令範圍內重新排序,而虛擬機器可以在更大層面、更多指令範圍內重新排序。硬體的重排序機制參考《從JVM並發看CPU記憶體指令重排序(Memory Reordering)》

重排序很不好理解,上面只是簡單地提了下其場景,要想較好地理解這個概念,需要構造一些例子和圖表,在這裡介紹兩篇介紹比較詳細、生動的文章《happens-before俗解》和《深入理解Java內存模型(二)——重排序》。其中的「as-if-serial」是應該掌握的,也就是:不管怎麼重新排序,單執行緒程式的執行結果不能被改變。編譯器、執行時間和處理器都必須遵守「as-if-serial」語意。拿個簡單範例來說,


public void execute(){
 int a=0;
 int b=1;
 int c=a+b;
}

這裡a=0,b=1兩句可以隨便排序,不影響程式邏輯結果,但c=a+b這句必須在前兩句的後面執行。

從前面那個例子可以看到,重排序在多執行緒環境下出現的機率還是挺高的,在關鍵字上有volatile和synchronized可以停用重排序,除此之外還有一些規則,也正是這些規則,使得我們在平時的程式工作中沒有感受到重排序的壞處。

程式次序規則(Program Order Rule):在一個執行緒內,依照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程式碼順序,因為要考慮分支、循環等結構。

監視器鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個物件鎖的lock操作。這裡強調的是同一個鎖,而「後面」指的是時間上的先後順序,如發生在其他執行緒的lock操作。

volatile變數規則(Volatile Variable Rule):對一個volatile變數的寫入操作發生於後面對這個變數的讀取操作,這裡的「後面」也指的是時間上的先後順序。

執行緒啟動規則(Thread Start Rule):Thread獨享的start()方法先行於此執行緒的每一個動作。

執行緒終止規則(Thread Termination Rule):執行緒中的每個操作都先行發生於對此執行緒的終止偵測,我們可以透過Thread.join()方法結束、Thread.isAlive()的返回值偵測到執行緒已經終止執行。

執行緒中斷規則(Thread Interruption Rule):對執行緒interrupte()方法的呼叫優先於被中斷執行緒的程式碼偵測到中斷事件的發生,可以透過Thread.interrupted()方法偵測執行緒是否已中斷。

物件終結原則(Finalizer Rule):一個物件的初始化完成(建構子執行結束)先行發生於它的finalize()方法的開始。

傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

正是以上這些規則保障了happen-before的順序,如果不符合以上規則,那麼在多執行緒環境下就不能保證執行順序等同於程式碼順序,也就是「如果在本執行緒中觀察,所有的操作都是有序的;如果在一個線程中觀察另外一個線程,則不符合以上規則的都是無序的”,因此,如果我們的多線程程序依賴於代碼書寫順序,那麼就要考慮是否符合以上規則,如果不符合就要透過一些機制使其符合,最常用的就是synchronized、Lock以及volatile修飾符。

以上是Java指令重排序的問題解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn