三面阿里竟然敗在了volatile關鍵字上(一文搞懂volatile 由淺入深,直擊痛點)

今天閒來無事跟同事小麥大叔閒聊,

SoWhat:麥叔聽說你偷偷面阿里啦,面的咋樣?
小麥大叔: 一面挺簡單的,主要問了一些基本的數據結構跟算法,還問了下 HashMap的十大常見基本問題。我都答案上來了,還問了我JDK7環,幸虧你那個HashMap環繪製的牛逼,我答的不錯就讓我準備二面了。
SoWhat:二面類?
小麥大叔:二面問了我一些JVM的問題,問我對於JVM內存模型的理解,還有GC的常見理解,最終還問了我下類加載機制,我看你之前水過這個 JVM系列,就依葫蘆畫瓢答上來了,讓我準備三面。
SoWhat:麥叔這波可以啊,三面問的啥啊?
小麥大叔:三面問了我一些CAS、Lock、AQS跟 ConcurrentHashMap 的底層實現什麼的,還問了我下線程池的七大參數跟四大拒絕策略,以及使用注意事項。我看你水過 併發編程系列,也就答上來了。
Sowhat:厲害啊這是要過的節奏阿!
小麥大叔:過個錘子,三面的這個總監最後竟然問了我下我對volatile的底層原理。你妹的你麼水,我就答了一些基本的可見性跟弱原子性,然後我感覺面試官不太滿意啊!
Sowhat:額好吧,那我抓緊再水文寫下個關於volatile的使用。

使用

volatile變量自身具有下列特性相信大家都知道:

  1. 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
  2. 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

其中第二點可以理解爲把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步,就跟下面的SoWhatSynSoWhat功能類似哦。

class SoWhat{
    volatile int i = 0; // volatile修飾的變量
    public int getI(){
        return i;// 單個volatile變量的讀
    }
    public  void setI(int j){
        this.i = j; // 單個volatile 變量的寫
    }
    public void inc(){
        i++;//複合多個volatile 變量
    }
}
class SynSoWhat{
     int i = 0;
    public synchronized int getI(){
        return i;
    }
    public  synchronized void setI(int j){
        this.i = j;
    }
    public void inc(){ // 普通方法調用
        int tmp = getI(); // 調用已同步方法
        tmp = tmp + 1;//普通寫方法
        setI(tmp);// 調用已同步方法
    }
}

寫理解

volatile寫的內存語義如下:

當寫一個volatile變量時,JMM會把該線程對應的本地中的共享變量值刷新到主內存。

public class VolaSemanteme {
	int a = 0;
	volatile boolean flag = false; // 這是重點哦
	public void init() {
		a = 1; 
		flag = true; 
		//.......
	}
	public void use() {
		if (flag) { 
			int i = a * a; 
		}
		//.......
	}
}

線程A調用init方法,線程B調用use方法。
在這裏插入圖片描述

讀理解

volatile讀的內存語義如下:

當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

public class VolaSemanteme {
	int a = 0;
	volatile boolean flag = false; // 這是重點哦
	public void init() {
		a = 1; 
		flag = true; 
		//.......
	}
	public void use() {
		if (flag) { 
			int i = a * a; 
		}
		//.......
	}
}

流程圖大致是這樣的:
在這裏插入圖片描述

volatile 指令重排

volatile 變量的內存可見性是基於內存屏障(Memory Barrier)實現。關於內存屏障的具體講解以前寫過不再重複,JMM裝逼於無形這裏說過。總結來說就是JMM內部會有指令重排,並且會有af-if-serialhappen-before的理念來保證指令重拍的正確性。內存屏障就是基於4個彙編級別的關鍵字來禁止指令重排的,其中volatile的重拍規則如下:

  1. 第一個爲讀操作時,第二個任何操作不可重排序到第一個前面。
  2. 第二個爲寫操作時,第一個任何操作不可重排序到第二個後面。
  3. 第一個爲寫操作時,第二個的讀寫操作也不運行重排序。

在這裏插入圖片描述

volatile寫底層實現

JMM對volatile的內存屏障插入策略

在每個volatile寫操作的前面插入一個StoreStore屏障。在每個volatile寫操作的後面插入一個StoreLoad屏障。

在這裏插入圖片描述

volatile 讀底層

JMM對volatile的內存屏障插入策略

在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個LoadStore屏障。

在這裏插入圖片描述

其中重點說下volatile讀後面爲什麼跟了個LoadLoad。加入我有如下代碼 AB兩個線程執行,B線程的flag獲取下面的讀被提前了。
在這裏插入圖片描述
在這裏插入圖片描述

volatile的實現原理

有volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令。在CPU級別的功能如下:

  1. 將當前處理器緩存行的數據寫回到系統內存
  2. 這個寫回內存的操作會告知在其他CPU你們拿到的變量是無效的下一次使用時候要重新共享內存拿。

我們可以通過jitwatch對簡單的代碼進行詳細的反彙編看一下。

package com.sowhat.demo;

public class VolaSemanteme {
    int unvloatileVal = 0;
    volatile boolean flag = false;

    public void init() {
        unvloatileVal = 1;
        flag = true; // 第九行哦
    }
    public void use() {

        if (flag) {
            int LocalA = unvloatileVal;
            if (LocalA == 0) {
                throw new RuntimeException("error");
            }
        }
    }
    public static void main(String[] args) {
        VolaSemanteme volaSemanteme = new VolaSemanteme();
        volaSemanteme.init();
        volaSemanteme.use();
    }
}

對普通變量的賦值操作:
在這裏插入圖片描述
volatile變量的賦值操作。
在這裏插入圖片描述
可以對比得出,volatile 修飾的變量確實會多一個 lock addl $0x0,(%rsp) 指令。

0x0000000114ce95cb: lock addl $0x0,(%rsp)  ;*putfield flag
  ; - com.sowhat.demo.VolaSemanteme::init@7 (line 9)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章