Java中的volatile關鍵字

 概要

若是不存在併發同步狀況時,編譯器或運行時或處理器會應用各類優化。而緩存和重排序則是併發上下文中的優化手段,Java和JVM提供了許多控制內存順序的方法,volatile關鍵字就是其中之一。程序員

沒有volatile會怎麼樣?

看下面的一個例子:面試

public class TaskRunner {

    private static int number;
    private static boolean ready;

    private static class Reader extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }

            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new Reader().start();
        number = 42;
        ready = true;
    }
}

TaskRunner類維護兩個簡單的變量。在它的main方法中,它建立了另外一個線程,只要它是false,它就會在ready變量上自旋。當變量變爲true時,線程將打印number變量。緩存

咱們指望這個程序在短暫的延遲後簡單地打印42。然而,實際上這個延遲可能會更長。它甚至可能永遠掛起,甚至打印0。多線程

這些異常的緣由是缺少適當的內存可見性和重排序,貼合本文來講,就是沒有使用volatile關鍵字修飾變量。併發

內存可見性

簡單來講,多線程運行在多個CPU上,而每一個線程都會有本身的的cache,所以沒法保證從主存中讀取數據的順序,即沒法保證各個CPU上的線程讀取的變量數據一致。ide

結合上面的程序,主線程在其核心緩存中保留了ready和number的副本,而Reader線程也是一樣保留了副本,以後主線程更新緩存值。在大多數現代處理器上,寫入請求在發出後不會當即應用。事實上,處理器傾向於將這些寫入排在一個特殊的寫入緩衝區中。一段時間後,它們會一次性將這些寫入應用到主內存中。性能

所以當主線程更新number和ready變量時,沒法保Reader線程會看到什麼。換句話說,Reader線程可能會當即看到更新的值,或者有一些延遲,或者根本不會。優化

重排序

上面提到過,除了一直死循環外,程序還有小几率打印出0,這就是重排序的緣由。在CPU執行指令時,先更新了ready變量而後執行的線程操做。spa

從新排序是一種用於提升性能的優化技術,不一樣的組件可能會應用這種優化:線程

  • 處理器能夠按程序順序之外的任何順序刷新其寫緩衝區
  • 處理器可能會應用亂序執行技術
  • JIT編譯器能夠經過從新排序進行優化
volatile關鍵字

那麼volatile關鍵字幹了什麼呢?

volatile關鍵字在彙編階段對變量加上Lock前綴指令,經過MESI緩存一致性協議來保證線程之間的可見性,任意線程對變量的修改都會被同一時間同步到全部讀取該變量的線程CPU上,簡單來講,一個改了就能保證全部的都改了。

這裏先看彙編層的Lock指令,早期CPU採起鎖總線的方式來實現這個指令,仲裁器選擇一個CPU獨佔總線,從而使其餘CPU沒法經過總線與內存通信,實現原子性;固然這種方式效率低,如今通常採用cache locking,這種場景下的數據一致是經過MESI緩存一致性協議來完成的。

這裏再也不詳細說明緩存一致性協議,主要思想是CPU會不斷嗅探總線上的數據交換,當一個緩存表明它所在的CPU去讀寫內存時,其餘CPU都會獲得通知,從而同步本身的緩存。

在Java內存模型中,存在着原子操做,這些原子操做與Java內存模型控制併發有着關鍵做用。

  • read(讀取):從主內存讀取數據
  • load(載入):將主內存讀取到的數據寫入工做內存,即緩存
  • use(使用):從工做內存讀取數據來計算
  • assign(賦值):將計算好的值從新賦值到工做內存中
  • store(存儲):將工做內存數據寫入主內存
  • write(寫入):將store過去的變量值賦值給主內存中的變量
  • lock(鎖定):將主內存變量加鎖,標識爲線程獨佔狀態
  • unlock(解鎖):將主內存變量解鎖,解鎖後其餘線程能夠鎖定該變量

在volatile關鍵字修飾下,store和write操做必須是連續的,組合成了原子操做,修改後必須當即同步到主內存,使用時必須從主內存刷新,由此保證volatile可見性。

同時,volatile關鍵字也採用內存屏障來禁止指令重排。volatile變量的內存可見性影響超出了volatile變量自己。

更具體地說,假設線程A寫入一個volatile變量,而後線程B讀取同一個volatile變量。在這種狀況下,在寫入volatile變量以前對A可見的值將在讀取volatile變量後對B可見:

a022d94d42d3f9a531c10360ec46ee69.png

從技術上講,對volatile字段的任何寫入都發生在同一字段的每次後續讀取以前。這是Java 內存模型的volatile變量規則。

因爲內存排序的長處,有時咱們能夠捎帶volatile的可見性屬性另外一個變量。例如,在咱們的示例中,咱們只須要將ready變量標記爲volatile:

public class TaskRunner {

    private static int number; // not volatile
    private volatile static boolean ready;

    // same as before
}

在讀取ready變量以後,任何在將ready變量寫爲true以前的內容對任何內容都是可見的。所以,number變量會捎帶上ready變量強制執行的內存可見性。簡而言之,即便它不是volatile變量,它也表現出volatile行爲。

經過利用這些語義,咱們能夠將類中的少數變量定義爲volatile並優化可見性。

最後

最近我整理了整套《JAVA核心知識點總結》,說實話 ,做爲一名Java程序員,不論你需不須要面試都應該好好看下這份資料。拿到手老是不虧的~個人很多粉絲也所以拿到騰訊字節快手等公司的Offer

Java進階之路羣,找管理員獲取哦-!

35c27fe92ab418c3158824ecb093111b.png

99d086cf20ee5916b4c79ed22bc55a6c.png

相關文章
相關標籤/搜索