Java面試題集錦(1):volatile關鍵字

qq_36755571發表於2020-12-12

Java面試題集錦(1):volatile關鍵字

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

  1. 保證可見性
  2. 不保證原子性
  3. 禁止指令重排序

1.JMM(java記憶體模型)

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

JMM關於同步的規定:

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

由於JVM執行程式的實體是執行緒,而執行緒建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),工作記憶體是每個執行緒的私有資料,而JAVA記憶體模型種規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問。 但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行。首先將變數從主記憶體中拷貝到自己的工作記憶體,然後對變數進行操作,操作完成後再將變數寫回主記憶體。 不能直接操作主記憶體中的變數。各個執行緒中的工作記憶體中儲存著主記憶體的變數拷貝副本,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒之間的通訊(傳值)必須通過主記憶體完成。

JMM執行緒的三大特性:
原子性、可見性、有序性

2.volatile程式碼演示可見性

class myData {
    volatile int number = 0;
    public void add(){
        this.number+=1;
    }
}

class VolatileDemo {
    public static void main(String[] args) {
        myData data1 = new myData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try{
                TimeUnit.SECONDS.sleep(3);
            }
            catch (InterruptedException e){
                e.printStackTrace();
            }
            data1.add();
            System.out.println(Thread.currentThread().getName()+"\t updated number's value: ");
        },"thread1").start();
        while(data1.number == 0){

        }
        System.out.println(Thread.currentThread().getName()+"\t main thread knows the change!");
    }
}

在這裡插入圖片描述
如果沒有volatile關鍵字,main執行緒無法看到number的變化,最後一句println永遠不會列印出來。

3.volatile不能保證原子性

原子性:不可分割,完整性,也即某個執行緒正在做某個具體業務時,中間不可以被阻塞或者分科,需要整體完成。同時成功/失敗。

import java.util.concurrent.TimeUnit;

class myData {
    volatile int number = 0;
    public void add(){
        this.number++;
    }
}

class VolatileDemo {
    public static void main(String[] args) {
        myData data1 = new myData();

        for(int i=0;i<20;i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data1.add();
                }
            }, String.valueOf(i)).start();
        }
        while(Thread.activeCount()>2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number's value: "+data1.number);
    }
}

在這裡插入圖片描述
如果保證原子性,結果應該是20000,正因為多個執行緒進行併發的時候,運算子無法保證原子性,所以結果小於20000(有狗屎運可以到20000)。用synchronized修飾add()方法,一定可以到20000。

volatile不保證原子性的問題解決

  1. synchronized(殺雞用牛刀)
  2. 使用原子類AtomicInteger
	AtomicInteger atomicInteger = new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }

4.volatile禁止指令重排序

計算機在執行程式時,為了提高效能,編譯器和處理器常常會對指令進行重排序,一般分為三種。
在這裡插入圖片描述

  1. 編譯器優化的重排序:編譯器在不改變單執行緒程式語義的前提下可以重排語 句的執行順序。
  2. 指令級並行的重排序:現代處理器採用了指令級並行技術 (Instruction-LevelParallelism,ILP)來將多條指令重疊執行,如果不存在 資料依賴性(如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作, 此時這兩個操作之間就存在資料依賴性。),處理器可以改變語句對應機器指令 的執行順序。
  3. 記憶體系統的重排序:由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存 操作看上去可能是在亂序執。

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

//例子1
public void mySort(){
	int x=11;//語句1
	int y=12;//語句2
	x=x+5;//語句3
	y=x*x;//語句4
}

單執行緒中正常的程式碼是1234,多執行緒中可能執行的順序是2134、1324等。但是不會重排為4123(因為有x和y資料依賴性)。
重排序可能導致資料執行結果不一致,volatile可以保證禁止重排。

//例子2
public class ReSortSeqDemo{
	int a=0;
	boolean flag=false;
	public void method01(){
		a=1;//語句1
		flag=true;//語句2
	}
	public void method02(){
		if(flag){
			a=a+5;//語句3
			System.out.println("retValue:  "+a);
		}
	}
}

指令重排之後,因為1和2沒有指令重排序,有可能發生23,則輸出a的結果為5。
指令重排序小結: volatile實現了禁止指令重排序,從而避免多執行緒環境下程式出現亂序執行的現象。
記憶體屏障(memory barrier): 又稱記憶體柵欄,是一個CPU指令,作用有1、保證特定操作的執行順序,2、保證某些變數的記憶體可見性(利用該特性實現了volatile的記憶體可見性)。
由於編譯器和處理器都能執行指令重排序,如果在指令間插入一條memory barrier,則會告訴編譯器和CPU,不管什麼指令都不能和這條memory barrier指令重排序,也就是說通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化。 記憶體屏障另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。

相關文章