一塊兒學併發編程 - Volatile關鍵字詳解

volatile是用來標記一個JAVA變量存儲在主內存(main memory)中,多線程讀寫volatile變量會先從高速緩存中讀取,可是寫入的時候會當即經過內存總線刷到主存,同時內存總線中會對這個變量進行監聽,當發現數據變更時,會主動將該變量的CPU Cache置爲失效。確切的說:每次寫操做volatile變量時,將直接將主內存(main memory)中最新的值讀取到當前Cache操做html

<!-- more -->java

概述

可見性: 是指線程之間數據可見共享,一個線程修改的狀態對另外一個線程是可見的。好比:用volatile修飾的變量,就會確保變量在修改時,其它線程是可見的。。git

在多線程中,對非volatile變量進行操做的時候,出於對性能的考慮,當對這些變量進行數據操做時,線程可能會從主內存裏拷貝變量到CPU Cache中去。多核CPU環境中,多個線程分別在不一樣的CPU中運行,就意味着,多個線程都有可能將變量拷貝到當前運行的CPU Cache裏。緩存

以下圖所示(多線程數據模型): 微信

多線程數據模型

示例 - 非Volatile

public class NotSharedObject {

    private static int COUNTER = 0;
    private static final int MAX_LIMIT = 5;

    public static void main(String[] args) {
        new Thread(() -> {
            int localValue = COUNTER;
            while (localValue < MAX_LIMIT) {
                if (localValue != COUNTER) {
                    System.out.printf("[線程] - [%s] - [%d]\n", Thread.currentThread().getName(), COUNTER);
                    localValue = COUNTER;
                }
            }
        }, "READER").start();

        new Thread(() -> {
            int localValue = COUNTER;
            while (COUNTER < MAX_LIMIT) {
                System.out.printf("[線程] - [%s] - [%d]\n", Thread.currentThread().getName(), ++localValue);
                COUNTER = localValue;
            }
        }, "UPDATER").start();
    }
}
[線程] - [UPDATER] - [1]
[線程] - [UPDATER] - [2]
[線程] - [UPDATER] - [3]
[線程] - [UPDATER] - [4]
[線程] - [UPDATER] - [5]
  • 結果代表,UPDATE線程雖修改數據,可是READER線程並未監聽到數據的變更,當前線程操做的是當前CPU Cache裏的數據,而不是從main memory獲取的。多線程

  • couner 的變量未使用volatile關鍵字修飾即JVM沒法保證有效的將CPU Cache的內容寫入主存中。意味着 counter 變量在CPU Cache中的值可能會與主存中的值不同。app

以下圖所示(無Volatile): 性能

無Volatile

示例 - Volatile

private static volatile int COUNTER = 0;


[線程] - [UPDATER] - [1]
[線程] - [UPDATER] - [2]
[線程] - [READER] - [1]
[線程] - [UPDATER] - [3]
[線程] - [UPDATER] - [4]
[線程] - [UPDATER] - [5]
[線程] - [READER] - [3]
  • 結果代表,volatile修飾後的變量並不會達到Lock的效果,它只會保證線程可見性,但不保證原子性,在讀取volatile變量和寫入它的新值時,因爲操做耗時較短,就會產生 競爭條件:多個線程可能會讀取到volatile變量的相同值,而後產生新值並寫入主內存,這樣將會覆蓋互相的值。(有興趣的能夠在建立一個UPDATE線程測試效果)測試

以下圖所示(Volatile): 優化

有Volatile

Happens-Before 原則

Java5以後volatile關鍵字不只能用於保證變量從主存中進行讀寫操做,同時還遵循Happens-Before原則,下文將會描述存在於volatile中的一些細節,想深刻的能夠自行谷歌 happens-before relationship或者訪問提供的幾個連接

參考文獻(1):https://en.wikipedia.org/wiki/Happened-before

參考文獻(2):http://preshing.com/20130702/the-happens-before-relation/

參考文獻(3):http://www.importnew.com/17149.html

  • 若是T1線程寫入了一個volatile變量而後T2線程讀取該變量,那麼T1線程寫以前對其可見的全部變量,T2線程讀取該volatile以後也會對其可見。

  • 禁止JVM指令重排優化,一旦被volatile修飾的變量,賦值後多執行了一個load addl $0x0, (%esp)操做,至關於多了一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障以前的位置),單核CPU訪問內存時,並不須要內存屏障;

看看下面這個示例:

T1線程:
Object obj;
volatile boolean init;
---------T1線程------------
obj = createObj()    1;
init = true;        2;
---------T2線程------------
while(!init){
    sleep();
}
useTheObj(obj);
  • volatile修飾過的變量 init在寫操做以前,建立了非volatile變量的obj,於是T1線程在寫入init後,會將obj也寫入主內存中去。

  • 因爲T2線程啓動的時候讀取被volatile修飾過的init,於是變量 init 和變量 obj 都會被寫入T2線程所使用的CPU緩存中去。當T2線程讀取 obj 變量時,它將能看見被T1線程寫入的東西。

總結適用場景

  • 線程可見,狀態量標記

volatile boolean start = true;

while(start){
//
}
void close(){
    start = false;
}
  • 屏障先後一致性,禁止指令重排

- 說點什麼

全文代碼:https://gitee.com/battcn/battcn-concurent/tree/master/Chapter1-1/battcn-thread/src/main/java/com/battcn/chapter13

  • 我的QQ:1837307557

  • battcn開源羣(適合新手):391619659

微信公衆號:battcn(歡迎調戲)

相關文章
相關標籤/搜索