【Java高級】volatile變量詳解

volatile概念

是java虛擬機提供的輕量級的同步機制

特性

保證可見性

說到volatile的可見性就要先說說JMM模型

JMM內存模型

JMM(Java內存模型)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中的各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

JMM關於同步的規定:

  • 線程解鎖前,必須把共享變量的值刷回主內存
  • 線程加鎖前,必須讀內存的最新值到自己的工作內存
  • 加鎖解鎖是同一把鎖

JMM特性

  1. 可見性
  2. 原子性
  3. 有序性

驗證volatile可見性

資源類

class MyData{
    int number=0;
    public void addTo60(){
        this.number=60;
    }
}

main方法

  public static void main(String[] args){
        MyData myData=new MyData();
        new Thread(()-> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            // 暫停線程
            try { TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}
            myData.addTo60();
            System.out.println((Thread.currentThread().getName()+"\t updated number value:" +myData.number));

        },"A").start();
        // 第2個線程就是我們的main線程
        while(myData.number==0){

        }
        System.out.println(myData.number);
        System.out.println(Thread.currentThread().getName()+"\t mission is over main get number:"+myData.number);
    }

執行結果
在這裏插入圖片描述
代碼停留在 while(myData.number==0){ },一直沒有動

分析:
這是因爲,在A線程中雖然修改了number=60,但是由於主內存修改後沒有辦法通知線程main,所以number此時仍爲0

優化
在這裏插入圖片描述
結果
在這裏插入圖片描述

分析:
明顯,main線程收到了A線程修改變量的通知,結果被打印出來

不保證原子性

原子性:不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分隔。需要整體完整,要麼同時成功,要麼同時失敗。

驗證volatile不保證原子性

資源類

class MyData{
    volatile int number=0;
    public void addTo60(){
        this.number=60;
    }

    public void addPlus(){
        this.number++;
    }
}

main

public static void main(String[] args){

        MyData myData=new MyData();

        for (int i = 1; i <=20 ; i++) {
            new Thread(()->{
                for (int j=1;j<=1000;j++){
                    myData.addPlus();
                }
            },String.valueOf(i)).start();
        }
        // 需要等待20個線程全部計算完成,再用main線程取得最後結果值
        while (Thread.activeCount()>2){
            Thread.yield();
        }
       System.out.println(Thread.currentThread().getName()+"\t final number value"+myData.number);
    }

執行結果
在這裏插入圖片描述
分析:
可以看出,外循環20次X內循環1000次調用add方法,結果應該是20000,但是總是加不夠,這就是因爲volatile不保證原子性
看下圖
在這裏插入圖片描述
某一時刻,A B C線程同時訪問主內存,然後都讀取了主內存中number變量的值0,當A將主內存中的0拷貝到工作內存,並加加的時候,此時由於JMM的特性,會將工作內存的值刷回主內存,但是如果由於一些原因A線程掛起,此時B線程將1刷回主內存,並且由於變量number由volatile修飾,具備可見性,會通知各線程,此時A線程不再掛起,由於線程很快,在B的修改還沒有通知到的時候,A將B修改的值再次修改爲1,此時A本來應該修改爲2,就發生了修改丟失的情況
在這裏插入圖片描述
這就是volatile的不保證原子性,那麼如何保證原子性呢?

解決不保證原子性方案

java.util.concurrent.atomic
資源類

lass MyData{
    volatile int number=0;
    public void addTo60(){
        this.number=60;
    }

    public void addPlus(){
        this.number++;
    }

    AtomicInteger atomicInteger=new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
}

main

public static void main(String[] args){


        MyData myData=new MyData();

        for (int i = 1; i <=20 ; i++) {
            new Thread(()->{
                for (int j=1;j<=1000;j++){
                    myData.addPlus();
                    myData.addMyAtomic();
                }
            },String.valueOf(i)).start();
        }
        // 需要等待20個線程全部計算完成,再用main線程取得最後結果值
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+"\t final number value"+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t atomicInteger value"+myData.atomicInteger);
    }

執行結果
在這裏插入圖片描述
結果就保證原子性了

禁止指令重排

爲了優化程序性能,編譯器和處理器會對java編譯後的字節碼和機器指令進行重排序,重排序分爲3種類型

  1. 編譯器優化的重排序,編譯器在不改變單線程語義的前提下,可以重新安排語句的執行順序
  2. 指令級並行的重排序
  3. 內存系統的重排序

從源碼到最終執行的指令序列的示意圖
在這裏插入圖片描述

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