多線程相關之線程可見

多線程相關複習

開胃菜

public class Test1 {

   private int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
                   
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("線程執行結束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

以上代碼在運行過程中發現他會一直卡住,不會輸出線程執行結束。

將代碼改造一下

public class Test1 {

   private int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
                   System.out.println();
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("線程執行結束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

我們在while(true)這一行下面加一個空的輸出語句,就可以打印出來了?爲什麼呢?

我們點開System.out.println()的源碼,可以發現

 

在這裏面加了synchorized關鍵字,就相當於是加了lock,對於底層指令而言,加入lock就相當於將工作內存中的值同步到住內存中

如果想要了解這個問題,首先我們要搞清楚java內存模型。

Java內存模型

 

主存與工作內存

java內存模型的主要目標是定義程序中各個變量的訪問規則,也就是在jvm中將變量存儲到內存和從內存中取出變量這樣的底層細節。

此處的變量與java編程所說的變量略有區別,主要是不包括局部變量和方法參數。因爲這兩個是線程私有的,不會被共享,自然就不存在競爭。

jmm規定了所有的變量都存儲在主存中。每條線程還有自己的工作內存,線程的工作內存中保存了被線程使用到的變量和主存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主存中的變量。不同線程之間也無法直接方位對方工作內存中的變量,線程變量值的傳遞需要通過主內存來完成。

內存交互

主存和工作內存之間的交互協議

JAVA內存存儲模型是通過動作(actions)的形式描述。這些動作也就是變量如何在主內存進入到工作內存、工作內存中的數據如何進入主內存。具體包括了:

lock unlock read load use assign store write。

lock 作用於主存變量,把一個變量標識成爲線程獨佔狀態

unlock 作用於主存變量,把一個鎖定的變量釋放出來,釋放後的變量才能被其他線程鎖定。

read 作用於主內存變量 ,把一個變量的值從主內存傳輸到線程的工作內存

load 作用於工作內存變量,把read操作從主內存中得到的變量值放入到工作內存變量副本

use 作用於工作內存變量,把工作內存中的一個變量的值傳遞給執行引擎,每當jvm遇到一個需要使用到變量的值的字節碼指令時會執行這個操作

assign 作用於工作內存變量,把一個從執行引擎收到的值付給工作內存變量,每當jvm遇到一個給變量賦值的指字節碼指令時執行這個操作

store作用於工作內存變量,把工作內存中一個變量的值傳給主存

write作用於主存變量,把store操作從工作內存中得到的變量放入到主存變量中

JMM還定義了執行上述八種操作必須滿足的規則
  1. 不允許read和load 、store和write操作之一單獨出現,也就是不允許一個變量從主存讀取了但工作內存不接收,或者從工作內存發起了回寫主存但主存不接收的情況出現

  2. 不允許一個線程丟棄他的最近的assign操作,也就是在工作內存中改變了之後必須把該變化同步回主內存

  3. 不允許一個線程沒有發生過任何assign操作,就把數據從線程的工作內存同步回主存

  4. 一個新的變量只能在主存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。也就是對一個變量實施的use和store操作之前,必須先執行過assign和load操作

  5. 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被一條線程重複執行多次,多次lock後,只有執行相同次數的unlock操作變量纔會被解鎖

  6. 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值

  7. 如果一個變量實現沒有被lock操作鎖定,則不允許對他進行unlock操作;也不允許unlock一個被其他線程鎖定主的變量

  8. 對一個變量執行unlock操作之前,必須先把此變量同步回主存,也就是執行store和write

先行發生原則 happens-before

Java語言中的先行發生原則在我們平時編碼中平時沒有怎麼注意到,但這個原則非常重要,是判斷線程是否安全的一個主要依據。

先行發生原則就是說:動作內部的偏序關係。線程A和線程B,如果A先行發生於B,那麼A所帶來的影響能夠同步到B,也就是說能被B發覺。這裏所謂的影響主要是指:共享變量的值。

這裏的先行發生原則主要有下面幾條:

程序次序法則:在同一個線程中,程序按照代碼的書寫順序執行。寫在前面的先執行,寫在後面的後執行(這裏要考慮流程控制語句)。

監視器鎖法則:一個unlock操作先行發生於後面對同一個鎖的lock操作。同一個鎖,後面指的是時間上先後順序

volatile變量法則:對被volatile修飾的變量,寫操作先行發生與後續對同一個變量的讀操作。

線程啓動法則:線程對象的啓動方法先行發生於此線程內部的每一個操作。

線程終止法則:線程對象中的所有操作都先行發生與線程的終止。

線程中斷法則:對線程interrupt方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。

對象終結法則:一個對象的構造方法結束先行於它的finalize方法的開始。

傳遞性:如果A先行發生與B,B先行發生於C,那麼A先行發生於C。

java無需任何同步手段保障就可以成立的規則。

回到之前的問題,我們再將代碼更改一下。

public class Test1 {

   private volatile int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
//                   System.out.println();
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("線程執行結束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

我們可以推斷出,是將主線程中的變量更改對另外一個線程可見,另外一個線程感知到了變量的更改,從而更改了數據,跳出了while循環。

volatile關鍵字?

1.保證一個線程對一個變量的修改,對另外的線程是可見的。前提是:對變量的修改不依賴變量原本得值。

2.保證不會發生指令重排序。

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