volatile可見性和指令重排


volatile關鍵字的2個作用

1.線程的可見性 

2.防止指令重排

 

什么是線程的可見性?

線程的可見性 就是一個線程對一個變量進行更改操作 其他線程獲取會獲得最新的值。

線程在執行的行 操作主線程的變量。會將變量的副本拷貝一份到線程的工作區域(避免每次到主線程讀取 提高效率),在更改后的一段時間內寫入主內存

如下示例代碼:

public class Accounting implements Runnable {
     boolean quit=false;
     int i=0;
    @Override
    public void run() {
        while (!quit){
            i++;
        }
        System.out.println("線程退出");
    }
    public static void main(String[] args) throws InterruptedException {
        Accounting accounting = new Accounting();
        Thread a1 = new Thread(accounting, "a1");
        Thread a2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println("開始通知線程結束");
                    accounting.setQuit(true);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        a2.start();
        a1.start();
        Thread.sleep(1000);
    }

    public boolean isQuit() {
        return quit;
    }

    public void setQuit(boolean quit) {
        this.quit = quit;
    }
}

 

這段代碼的邏輯就是線程a1 執行循環操作  a2 2秒后設置quit為true任務結束 打印 "線程退出";

那么真的能夠成功退出嗎?我們看看 線程執行在內存中的操作圖

打印:

開始通知線程結束

 

a2 線程首先將自己工作線程的quit改為ture ,然后一定時間之后去將主內存的quit改為true  ,但是a1線程始終是操作的是自己的工作內存的副本 所以死循環

這個時候在quit加上volatile關鍵字

  volatile boolean quit=false;

打印

開始通知線程結束
線程退出

加上volatile關鍵字后。當一個線程對變量進行修改會更新自己的工作內存里面的值,然后立即將改動的值刷新到主內存,同時線程2的工作內存的quit副本緩存失效  下次直接到主內存讀取  所以能夠正常執行

記錄一個小插曲

System.out.println,sychronized,Thread.sleep Thread.sleep 影響可見性?

System.out.println

public class Accounting implements Runnable {
     boolean quit=false;
     int i=0;
    @Override
    public void run() {
while (!quit){ i++; System.out.println(i); } System.out.println("線程退出"); } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread a1 = new Thread(accounting, "a1"); Thread a2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); System.out.println("開始通知線程結束"); accounting.setQuit(true); } catch (InterruptedException e) { e.printStackTrace(); } } }); a2.start(); a1.start(); Thread.sleep(1000); } public boolean isQuit() { return quit; } public void setQuit(boolean quit) { this.quit = quit; }

會發現沒有加上volatile一樣可以成功退出 。那我們上面說的 線程的內存處理 不成立了嗎?

查資料說 是因為jvm對鎖的優化。因為如果我們在循環里面加上sychronize同步鎖 會產生大量的鎖競爭 所以jvm優化過后

   synchronized (this){
            while (!quit){
              //.....
            }
        }

但是我們並沒有在while里面加鎖啊。我們看看打印的方法源碼

    public void println(int x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

sleep方法並沒有加鎖,為什么能夠保證可見性

sleep是阻塞線程並不釋放鎖,讓出cpu調度。 讓出cpu調度后下次執行會刷新工作內存

指令重排

指令重排指在編譯的時候,在不單線程運行不影響結果的情況下進行指令優化

如:

public class Context {
    boolean isLoad=false;
    Object configuration=null;
    public void   loadConfiguration(){
        System.out.println("正在加載配置文件");
        configuration=  new Object();
        isLoad=true;
    }

    public void  initContext(){
        System.out.println("正在進行初始化");
    }

    public static void main(String[] args) {
       Context context=new Context();
       context.loadConfiguration();
       if(context.isLoad){
           context.initContext();
       }
    }
}

這段代碼就是先加載配置文件信息  然后初始化上下文

我們在單線程下 把他們的順序調換模擬指令重排 會對結果沒有影響

  public void   loadConfiguration(){
        isLoad=true;
        System.out.println("正在加載配置文件");
        configuration=  new Object();
       
    }

但是在多線程下面

public class Context {
     boolean isLoad=false;
    Object configuration=null;
    public void   loadConfiguration(){
        //模擬jvm指令重排 將isLoad命令排在第一位
        isLoad=true;
        /***
         * 模擬並發情況下指令重排。導致的isload=true排到前面。
         * 這個時候配置文件沒初始化。initContext監聽到lsLoad等於true根據配置文件進行初始化
         */
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        configuration=  new Object();
        //isLoad=true;指令重排前
    }

    public void  initContext(){
        configuration.toString();
        System.out.println("正在進行初始化");
    }

    public static void main(String[] args) {
        Context context=new Context();
        //負責監聽 如果加載完畢 則進行上下午初始化
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {

                    while (true){
                        if(context.isLoad){
                            context.initContext();
                            break;
                        }
                    }


            }
        },"t2");
        //負責加載配置文件
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                context.loadConfiguration();
            }
        },"t1");
        t1.start();
        t2.start();
    }
}

 

只是模擬指令重排 先不考慮可見性  這種情況會初始化context 沒有configuration 報錯  使用volatile關鍵字修飾可以避免

值得注意的一點

volatile雖然能夠保證線程的可見性 但是並不能保證原子性  比如i++操作 都是讀出i的值 進行運算再寫入。如果在讀出的時候別的線程改變了 就會不一致

哪種場景適合用volatile 對一個變量的值進行修改 不依賴其他值。 比如 index=true   而不是i=i+j;或則index=j>a   或 a=j (會從內存中讀出j的值 然后賦值到a);

java提供atomic cas能夠性能比鎖高能夠保證原子性 如:atomicInt atomictDouble


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM