關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制,可是它並不容易徹底被正確、完整地理解,以致於不少程序員都習慣不去實習它,使用它。學校中對於volatile關鍵字也是一筆帶過,並未深入講解,而工做中,不少程序員都習慣使用synchorized來處理同步問題,而且聽信不少古老信條「不能理解好volatile便不要去用它」,可是這麼作是捨本逐末,本末倒置的處理方式。深刻了解volatile關鍵字以及底層虛擬機實現細節,對於理解Java併發特性有着深入的意義。java
volatile是一個類型修飾符(type specifier),就像你們更熟悉的const同樣,它是被設計用來修飾被不一樣線程訪問和修改的變量。volatile的做用是做爲指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。程序員
—— 摘自百度百科編程
衆所周知,併發編程是編程中一個難點,可謂之小高山。在Java的生態圈中,併發編程的難度被極爲完善的類庫所下降,但一樣的,這些類庫也就造就了一大批知其然而不知其因此然的程序猿,況且類庫並不能完美解決任何一個業務需求(JDK提供的強大類庫有利亦有弊)。只有深知Java虛擬機底層對於Java併發編程的支持與缺陷,才能在合適的場景選擇合適的類庫,甚至本身實現類庫以解決更加複雜的業務場景。想要真正理解併發編程,筆者認爲,不妨靜下心來從底層原理啃下去,由於當你從底層原理來看待上層應用的併發問題的時候,思路將更加清晰。api
爲何會有併發問題?緩存
稍微對計算機有所瞭解的猿們都明白,當前計算機的存儲設備的運算能力與處理器的運算能力是有幾個數量級的差距。木桶定理:決定它到底能裝多少水不是由最高的那根木板所決定,而是由最低的那塊木板所決定。根據木桶定理,決定計算機的運算速率與性能的,無疑是計算機當前的存儲設備(想要了解計算機各個構件之間的運算速度,請自行百度)。爲了讓計算機能夠跑得更快,計算機先驅者在處理器與儲存設備之間引入了一層運算速度很是接近處理器的高速緩存:將運算須要使用到的數據複製到緩存中,讓運算快速進行,當運算結束後再從緩存同步回內存之中,這樣子處理器就無須等待緩慢的內存讀寫。可是,在多處理器系統中,每一個處理器都對應着一個高速緩存,而這些高速緩存又共享同一個主內存,因此就會引入一個新的問題——緩存一致性。除了增長高速緩存以外,處理器內部也會對輸入代碼進行亂序執行優化,與處理器的亂序執行優化相似,Java虛擬機的即時編譯也有相似的指令重排序優化。安全
因此併發問題的來源,無非就是多個高速緩存在共享同一個主內存時,若是多個高速緩存共享數據(好比變量)的狀況下,就有可能出如今某一個時刻中高速緩存記錄的同一個值得狀態的不一致,爲了解決緩存不一致的問題,先驅者們引入了緩存一致性協議(總線鎖機制)。架構圖以下:多線程
Java內存模型架構
Java虛擬機規範中試圖定義一種Java內存模型來屏蔽各類硬件操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。(這種設計思想在Java編程中可謂是無處不在,經過抽象來屏蔽底層實現細節,只對上層提供對外透明的接口)。併發
從Java虛擬機的角度來看內存分配問題,每個Java線程都分配了各自的工做內存,而且各個工做內存之間並不透明,而同時它們共享着同一塊主內存,因此併發的問題在於各個線程的工做內存與主內存交互過程當中產生的數據狀態不一致的問題.app
貫穿Java併發編程的全部問題,莫不是圍繞着如下三個原則來展開,並由點及面的產生了很是多的不一樣場景下使用的類庫(由於是圍繞着Java技術生態來說解,因此就採用Java生態的技術體系來分析三個原則,如下原文來自《深刻理解Java虛擬機》)而且,Java內存模型也是在圍繞着併發過程當中如何處理該三個原則來創建的:
原子性:原子具備不可分割性。由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,能夠大體認爲對基本數據類型的訪問讀寫是具有原子性的(除開long和double的非原子性協定)。
可見性:可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量這種依賴主內存做爲傳遞媒介的方式實現可見性的。不管是普通變量仍是volatile變量都是如此,它們的區別在於,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。注:除了volatile以外,synchorized和final也能夠實現可見性,synchorized關鍵字是採用同步快形式保證可見性,而final則是:被fianl修飾的字段在構造器中一旦初始化完成,而且構造器並無把「this」的引用傳遞出去(this引用逃逸),那麼其餘線程就能看到final字段的值。
有序性:Java內存模型的有序性在前面講解volatile時也詳細的討論過了,Java程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的(線程內表現爲串行的語義),若是在另外一個線程中觀察另外一個線程,全部操做都是無序的(指令重排序以及工做內存與主內存同步延遲現象)。volatile自己包含了禁止指令重排序的語義,而synchorized則是「一個變量在一個時刻只容許一條線程對其進行lock操做」。
Java內存模型定義了volatile變量的特殊性,天上具有了三個原則中的可見性以及有序性。爲何說volatile具有可見性呢?當一個變量被聲明爲volatile的時候,它將具有以下兩個特性:
一、保證此變量對於全部線程的可見性,這裏的可見性指的是當多個線程同時共享該變量時,一個線程對於該變量的修改,另外的線程能夠馬上感知其新值。(在各個線程的工做內存中,volatile變量也不存在不一致的狀況,可是因爲每次使用以前都要先刷新,執行引擎看不到不一致的狀況,由於能夠認爲不存在不一致性問題)。可是,Java內存模型規定了volatile變量的內存可見性,卻並未規定它的運算過程的原子性,因此在多線程併發場景下,volatile的運算操做並不是是線程安全。
二、禁止指令重排序優化。普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼的執行順序一致。
比方說以下的代碼:
Map configOptions; char[] configText; // 思考此變量爲什麼必定要申明爲volatile volatile boolean initialized = false; // 假設一下代碼在線程A中執行 // 模擬讀取配置文件,並將讀完完載入內存後將initilized設置爲true以通知其餘線程配置可用。 configOptions = new HashMap<>(); configText = readConfigFile(fileName); processConfigOptions(configText, configOptions); initilized = true; // 假設一下代碼在線程B執行 // 等待initlized爲true,表明線程A已經把配置信息初始化完成 while(!initilized) { sleep(); } // 使用線程A讀取的配置信息 doSomethingWithConfig();
若是線程A的執行代碼在尚未讀取完配置信息的時候,先執行了initlized的賦值操做,那麼線程B就能夠在線程A還未完成讀取操做的時候,去使用配置信息,結果天然就會發生重大錯誤。指令重排序是機器指令優化操做,屬於系統優化級別。
那麼volatile是如何禁止系統指令的重排序的呢?
在將變量定位volatile的時候,若是對volatile變量進行操做,Java虛擬機對在其操做(賦值,運算等等)的時候,插入一道內存屏障指令。內存屏障指令的做用就是保證對變量的操做修改會即時同步到主內存中,而緩存一致性(總線鎖機制)會經過一種嗅探指令來標識其餘線程當前記錄的該變量的值爲失效狀態,那麼當他們須要訪問該變量之時會主動發起指令從主內存中同步該變量的真實值。
簡單的總結來看,從分析了爲何會有併發的問題,再到Java內存模型,以及它是如何解決併發的三個原則(特性),再回過頭來思考併發,彷佛也就是那麼一回事。那麼再來看volatile變量,底層對它作了什麼,是如何保證可見性以及有序性,再從新回頭來思考volatile的適用場景,以及如何在synchorized與volatile中抉擇,也就能夠明白了。
volatile變量的具有多線程可見性,以及禁止指令重排序的能力,可是自己的運算並不是原子操做,因此volatile的適用場景可簡單總結爲兩點:
A、運算結果不依賴變量的當前值,或者可以保證只有單一的線程修改變量的值。
B、變量不須要與其它的狀態變量共同參與不變約束。
至於如何在synchorized以及volatile變量中抉擇?volatile變量讀操做的性能消耗與普通變量幾乎沒有什麼差異,可是寫操做則可能會慢一些,由於須要在本地代碼中插入許多內存屏障指令來保證處理器不會發生亂序執行。因此建議優先考慮volatile變量,只有當volatile的能力並不足以解決業務場景的狀況下再考慮synchorized以及juc併發編程包下的api。
可是若是全部的有序性都須要依賴voaltile和synchorized來完成,那麼不少操做就會變得很是繁瑣,而咱們再編寫併發代碼的時候並未有所察覺,是由於Java語言中的一個happend-before原則。它做爲一種規範指導咱們如何判斷數據是否存在競爭、線程是否安全。經過這些規則(規範)咱們能夠解決併發環境下兩個操做之間是否存在衝突的全部問題,再也不侷限與volatile與synchorized的可見性或者原子同步,Java內存模型下一些「自然的」先行發生關係,這些先行發生關係無需任何同步器協助就已經存在。。(如下先行發生原則摘抄自《深刻理解Java虛擬機》)
以上學習主要來自於《深刻理解Java虛擬機》,做者周志明周老師,寫的很是很是棒,很是推薦。