線程安全之可見性(一)

一:舉個栗子   

先舉個例子:

public class ThreadVolidate {
    public static int i = 0;
    public static Boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程運行了");
                while (flag) {
                    i++;
                }
                System.out.println("i== " + i);
            }
        });
        thread.start();
        Thread.sleep(3000);
        flag = false;
        System.out.println("shutdown......");
    }
}

       上述代碼可以看到:在主線程main方法中,啓動了一個子線程,子線程所做的事情,就是對i執行++操作,直到flag爲false退出循環,輸出i的值。主線程做的事情是,啓動子線程,休眠3s後,把flag置爲false,然後結束。

       經過上面的分析,來打印下執行結果:

       what?並沒有打印出i的值,這是爲什麼呢?

       經過對代碼的分析,可以得到應該會打印i的值,可是最後並沒有,那麼可以判斷,子線程在執行的過程中,並沒有退出while循環,這是爲何?

二:延申分析

1:CPU高速緩存

       爲了提高CPU的性能,CPU有高速緩存這麼個東西,具體的概念可以去百度看下,對此我瞭解的並不深。

       看下上圖:線程再去寫或者讀數據的過程中,會先去高速緩存中讀寫,在短時間內,主線程和子線程的數據可能會出現不一致的情況,但是呢,不會再3s這麼長的時間,所以上面代碼出現的問題,並不是高速緩存導致的,高速緩存並不願意去背鍋,那麼是什麼導致的呢?

2:指令重排

       在JVM執行字節碼的過程中,會對字節碼指令進行重排序,來提高執行的效率,但是要保證單線程內最後的執行結果不能發生改變,在多線程中,可能就會因爲指令重排導致某個線程執行的結果發生錯誤;下面看下例子:

      上圖中,假設線程1和線程2同時間執行,那麼最終的結果是:線程1中:a=b,c=1;線程2中:d=c,e=f/2,假設c的初始值是0,那麼線程2中d就是等於0,下面指令重排序後,會發生什麼呢?

       假設上圖是指令重排之後的結果:可以看到線程1進行了重排序,線程2還是原有順序執行,可以看出,線程2中d的最終結果發生了變化,變成了1,這就是指令重排會導致的問題。

3:腳本代碼和編譯代碼

腳本代碼和編譯代碼是有着區別的,具體是什麼呢?

1:腳本代碼(解釋執行):它會把代碼,一行一行的翻譯執行;

2:編譯代碼(編譯執行):會把一整塊代碼,編譯後執行。

而Java是介於腳本代碼和編譯代碼之間的。

java源代碼會被編譯成class文件,用到的是執行前編譯器,而運行時編譯器,就是常說的JIT編譯器,看下圖:

       可以看到,JIT編譯器中有解釋執行和編譯執行,當一個方法被多次調用或者方法中的循環體被多次循環,那麼就會從解釋執行轉爲編譯執行。而我們知道在JVM的方法區中,有JIT編譯後的代碼,他會對這樣的代碼,進行優化,如上圖編譯執行指向的代碼,這樣他就把true這麼一個值,緩存在裏面了,那麼就導致了上面代碼沒法打印出i的值,這就是上面代碼出現問題的根本原因。

     怎麼解決呢?在flag修飾符中加上volatile就可以解決了。

4:volatile關鍵字

 用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最的值。

 

反編譯之後可以看到,它有個ACC_VOLATILE標誌。

  具體怎麼實現:

  1:禁止緩存,也就是volatile修飾的變量禁止緩存,這樣寫入的時候就沒有緩存了,那麼CPU高速緩存導致的一點問題就可以解決了。

2:volatile修飾的,不能進行指令重排序,也就是說上面說的方法區緩存那一部分也沒有了,這樣就可以保證打印出i的值了:

public class ThreadVolidate {
    public static int i = 0;
    public static volatile Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程運行了");
                while (flag) {
                    i++;
                }
                System.out.println("i== " + i);
            }
        });
        thread.start();
        Thread.sleep(3000);
        flag = false;
        System.out.println("shutdown......");
    }
}

打印結果:

 

 

 

 

 

 

 

 

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