1. 程式人生 > 實用技巧 >關鍵字:volatile

關鍵字:volatile

介紹

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

三大特性:

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排

JMM記憶體模型之可見性

JMM:java記憶體模型是一種抽象概念,並不真實存在,他描述的是一組規則或規範,通過這組規範定義了程式中各個變數(例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。

JMM關閉同步的規定:

  • 執行緒解鎖前,必須把共享變數的值重新整理回主記憶體
  • 執行緒解鎖前,必須讀取主記憶體的最新值到自己的工作記憶體
  • 加鎖解鎖是同一把鎖

JMM規定了所有的變數都儲存在主記憶體(Main Memory)中。每個執行緒還有自己的工作記憶體(Working Memory),執行緒的工作記憶體中儲存了該執行緒使用到的變數的主記憶體的副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數(volatile變數仍然有工作記憶體的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主記憶體中讀寫訪問一般)。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒之間值的傳遞都需要通過主記憶體來完成。

可見性程式碼驗證

可見性:當本執行緒修改了自己本地記憶體的共享變數值,並寫回給主記憶體,其他執行緒第一時間就會知道該值修改了。

public class VolatileDemo {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t  come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.add();
            System.out.println(Thread.currentThread().getName()+"\t  updated num value:"+data.number);
        },"aaa").start();
        while (data.number==0){
            //main執行緒一直等待迴圈,直到number值不等於0
        }
       System.out.println(Thread.currentThread().getName()+"\t  mission is over,value:"+data.number);
    }
}

class Data{
    int number = 0;

    public void add(){
        this.number = 60;
    }
}

當執行緒修改了number值後,發現主執行緒並沒有感知到number的變化。

修改並執行

volatile int number = 0;

這裡就證明了volatile的可見性。

原子性

volatile不保證原子性。

原子性解釋:不可分割,完成性。某個執行緒在做某個具體業務時,中間業務不可以被加塞或者被分割,需要整體完成,要麼同時成功,要麼同時失敗。

案例演示程式碼:

class Data{
    volatile int number = 0;

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

public class VolatileDemo {
    public static void main(String[] args) {
        Data data = new Data();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    data.addPlus();
                }
            },String.valueOf(i)).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t number value:"+data.number);
    }
}

按照正常邏輯,20個執行緒一起跑,最後結果應該是20000,經過我長時間測試,極少情況下number最終等於20000,絕大部分都是小於20000的結果。

所以volatile不保證原子性。

不保證原子性的解釋

public class T1 {
    volatile int n =0;
    public void add(){
        n++;
    }
}

先配置idea external tools

配置成功後,右擊,執行javap -c

以下是列印結果:

Compiled from "T1.java"
public class com.wj.volat.T1 {
  volatile int n;

  public com.wj.volat.T1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field n:I
       9: return

  public void add();
    Code:
       0: aload_0       //從區域性變數0中裝載引用型別值
       1: dup           //複製棧頂部一個字長內容
       2: getfield      #2 //獲取n值       // Field n:I
       5: iconst_1        //將常量壓入棧中
       6: iadd          //加1操作
       7: putfield      #2  //將增加後的值寫回    // Field n:I
      10: return
}

n++被拆分成三個指令:getfield,iadd,putfield。

當一個執行緒修改完變數值後,正準備向主記憶體寫入變數值,卻不幸被掛起了,別的執行緒又去修改變數值並寫入主記憶體,但是原來那個執行緒並不知道,又被喚醒執行,寫入之前的修改值,導致最新修改值丟失。最終導致小於20000。

解決不保證原子性

第一種(不推薦):給add方法新增同步synchronized。

第二種:juc包下的Atomic工具

class Data{

    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic(){
        atomicInteger.getAndAdd(1);
    }
}

public class VolatileDemo {
    public static void main(String[] args) {
        Data data = new Data();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    data.addAtomic();
                }
            },String.valueOf(i)).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t number value:"+data.atomicInteger);
    }
}

使用AtomicInteger後就可以保證原子性

禁止指令重排

計算機在執行程式時,為提高效能,編譯器和處理器的常常會對指令做重排.

分為以下三種:

  • 單執行緒環境裡面確保程式最終執行結果和程式碼順序執行的結果一致
  • 處理器在進行指令重排序時必須要考慮指令之間的資料依賴性
  • 多執行緒環境中執行緒交替執行,由於編譯器優化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的,結果不可預測

出於效能考慮,JVM和CPU是允許對程式中的指令進行重排的,只要保證(重排後的)指令語義一致即可。如下程式碼為例:

int a = 1;
int b =2 ;
a++;
b++;

這些指令可以按以下順序重排,而不改變程式的語義:

int a = 1;
a++;
int b =2;b++;

volatile實現禁止指令重排優化,從而避免多執行緒環境下程式出現亂序執行的現象。

先了解一個概念,記憶體屏障(Memory Barrier)又稱記憶體柵欄,是一個CPU指令,它的作用有兩個:保證特定操作的執行順序和保證某些變數的記憶體可見性(利用該特性實現volatile的記憶體可見性)

由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就足說通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化。記憶體屏障另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。