volatile的做用程序員
volatile可使得在多處理器環境下保證了共享變量的可見性,那麼到底什麼是可見性呢?不知道你們有沒有思考過這個問題算法
在單線程的環境下,若是向一個變量先寫入一個值,而後在沒有寫干涉的狀況下讀取這個變量的值,那這個時候讀取到的這個變量的值應該是以前寫入的那個值。這原本是一個很正常的事情。可是在多線程環境下,讀和寫發生在不一樣的線程中的時候,可能會出現:讀線程不能及時的讀取到其餘線程寫入的最新的值。這就是所謂的可見性數組
爲了實現跨線程寫入的內存可見性,必須使用到一些機制來實現。而volatile就是這樣一種機制緩存
查看代碼的彙編指令,安全
會發現,在修改帶有volatile修飾的成員變量時,會多一個lock指令。lock是一種控制指令,在多處理器環境下,lock彙編指令能夠基於總線鎖或者緩存鎖的機制來達到可見性的一個效果。多線程
從硬件層面瞭解可見性的本質架構
一臺計算機中最核心的組件是CPU、內存、以及I/O設備。在整個計算機的發展歷程中,除了CPU、內存以及I/O設備不斷迭代升級來提高計算機處理性能以外,還有一個很是核心的矛盾點,就是這三者在處理速度的差別。CPU的計算速度是很是快的,內存次之、最後是IO設備好比磁盤。而在絕大部分的程序中,必定會存在內存訪問,有些可能還會存在I/O設備的訪問併發
爲了提高計算性能,CPU從單核升級到了多核甚至用到了超線程技術最大化提升CPU的處理性能,可是僅僅提高CPU性能還不夠,若是後面二者的處理性能沒有跟上,意味着總體的計算效率取決於最慢的設備。爲了平衡三者的app
速度差別,最大化的利用CPU提高性能,從硬件、操做系統、編譯器等方面都作出了不少的優化異步
1. CPU增長了高速緩存
2. 操做系統增長了進程、線程。經過CPU的時間片切換最大化的提高CPU的使用率
3. 編譯器的指令優化,更合理的去利用好CPU的高速緩存
而後每一種優化,都會帶來相應的問題,而這些問題也是致使線程安全性問題的根源。爲了瞭解前面提到的可見性問題的本質,咱們有必要去了解這些優化的過程
CPU高速緩存
線程是CPU調度的最小單元,線程設計的目的最終仍然是更充分的利用計算機處理的效能,可是絕大部分的運算任務不能只依靠處理器「計算」就能完成,處理器還須要與內存交互,好比讀取運算數據、存儲運算結果,這個I/O操做是很難消除的。而因爲計算機的存儲設備與處理器的運算速度差距很是大,因此現代計算機系統都會增長一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存和處理器之間的緩衝:將運算須要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中。
經過高速緩存的存儲交互很好的解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來了更高的複雜度,由於它引入了一個新的問題,緩存一致性。
什麼叫緩存一致性呢?
首先,有了高速緩存的存在之後,每一個CPU的處理過程是,先將計算須要用到的數據緩存在CPU高速緩存中,在CPU進行計算時,直接從高速緩存中讀取數據而且在計算完成以後寫入到緩存中。在整個運算過程完成後,再把緩存中的數據同步到主內存。
因爲在多CPU種,每一個線程可能會運行在不一樣的CPU內,而且每一個線程擁有本身的高速緩存。同一份數據可能會被緩存到多個CPU中,若是在不一樣CPU中運行的不一樣線程
看到同一分內存的緩存值不同就會存在緩存不一致的問題
爲了解決緩存不一致的問題,在CPU層面作了不少事情,主要提供了兩種解決辦法
1. 總線鎖
2. 緩存鎖
總線鎖和緩存鎖
總線鎖,簡單來講就是,在多cpu下,當其中一個處理器要對共享內存進行操做的時候,在總線上發出一個LOCK#信號,這個信號使得其餘處理器沒法經過總線來訪問到共享內存中的數據,總線鎖定把CPU和內存之間的通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,這種機制顯然是不合適的
如何優化呢?最好的方法就是控制鎖的保護粒度,咱們只須要保證對於被多個CPU緩存的同一份數據是一致的就行。因此引入了緩存鎖,它核心機制是基於緩存一致性協議來實現的。
緩存一致性協議
爲了達到數據訪問的一致,須要各個處理器在訪問緩存時
遵循一些協議,在讀寫時根據協議來操做,常見的協議有MSI,MESI,MOSI等。最多見的就是MESI協議。接下來給你們簡單講解一下MESI
MESI表示緩存行的四種狀態,分別是
1. M(Modify) 表示共享數據只緩存在當前CPU緩存中,而且是被修改狀態,也就是緩存的數據和主內存中的數據不一致
2. E(Exclusive) 表示緩存的獨佔狀態,數據只緩存在當前CPU緩存中,而且沒有被修改
3. S(Shared) 表示數據可能被多個CPU緩存,而且各個緩存中的數據和主內存數據一致
4. I(Invalid) 表示緩存已經失效
在MESI協議中,每一個緩存的緩存控制器不只知道本身的讀寫操做,並且也監聽(snoop)其它Cache的讀寫操做
對於MESI協議,從CPU讀寫角度來講會遵循如下原則:
CPU讀請求:緩存處於M、E、S狀態均可以被讀取,I狀態CPU只能從主存中讀取數據
CPU寫請求:緩存處於M、E狀態才能夠被寫。對於S狀態的寫,須要將其餘CPU中緩存行置爲無效纔可寫
總結可見性的本質
因爲CPU高速緩存的出現使得若是多個cpu同時緩存了相同的共享數據時,可能存在可見性問題。也就是CPU0修改了本身本地緩存的值對於CPU1不可見。不可見致使的後果是CPU1後續在對該數據進行寫入操做時,是使用的髒數據。使得數據最終的結果不可預測。
你可能但願在代碼裏面去模擬一下可見性的問題,實際上,這種狀況很難模擬。由於咱們沒法讓某個線程指定某個特定CPU,這是系統底層的算法,JVM應該也是無法控制的。還有最重要的一點,就是你沒法預測CPU緩存何時會把值傳給主存,可能這個時間間隔很是短,短到你沒法觀察到。最後就是線程的執行的順序問題,由於多線程你沒法控制哪一個線程的某句代碼會在另外一個線程的某句代碼後面立刻執行。
因此咱們只能基於它的原理去了解這樣一個存在的客觀事
瞭解到這裏,你們應該會有一個疑問,剛剛不是說基於緩存一致性協議或者總線鎖可以達到緩存一致性的要求嗎?爲何還須要加volatile關鍵字?或者說爲何還會存在可見性問題呢?
MESI優化帶來的可見性問題
MESI協議雖然能夠實現緩存的一致性,可是也會存在一些問題。
就是各個CPU緩存行的狀態是經過消息傳遞來進行的。若是CPU0要對一個在緩存中共享的變量進行寫入,首先須要發送一個失效的消息給到其餘緩存了該數據的CPU。而且要等到他們的確認回執。CPU0在這段時間內都會處於阻塞狀態。爲了不阻塞帶來的資源浪費。在cpu中引入了StoreBufferes。
CPU0只須要在寫入共享數據時,直接把數據寫入到storebufferes中,同時發送invalidate消息,而後繼續去處理其餘指令。
當收到其餘全部CPU發送了invalidate acknowledge消息時,再將storebufferes中的數據數據存儲至cache line中。最後再從緩存行同步到主內存。
可是這種優化存在兩個問題
1. 數據何時提交是不肯定的,由於須要等待其餘cpu給回覆纔會進行數據同步。這裏實際上是一個異步操做
2. 引入了storebufferes後,處理器會先嚐試從storebuffer中讀取值,若是storebuffer中有數據,則直接從storebuffer中讀取,不然就再從緩存行中讀取
咱們來看一個例子
exeToCPU0和exeToCPU1分別在兩個獨立的CPU上執行。假如CPU0的緩存行中緩存了isFinish這個共享變量,而且狀態爲(E)、而Value多是(S)狀態。
那麼這個時候,CPU0在執行的時候,會先把value=10的指令寫入到storebuffer中。而且通知給其餘緩存了該value變量的CPU。在等待其餘CPU通知結果的時候,CPU0會繼續執行isFinish=true這個指令。
而由於當前CPU0緩存了isFinish而且是Exclusive狀態,所
以能夠直接修改isFinish=true。這個時候CPU1發起read操做去讀取isFinish的值可能爲true,可是value的值不等於10。
這種狀況咱們能夠認爲是CPU的亂序執行,也能夠認爲是一種重排序,而這種重排序會帶來可見性的問題
這下硬件工程師也抓狂了,咱們也能理解,從硬件層面很難去知道軟件層面上的這種先後依賴關係,因此沒有辦法經過某種手段自動去解決。
因此硬件工程師就說:既然怎麼優化都不符合你的要求,要不你來寫吧。
因此在CPU層面提供了memory barrier(內存屏障)的指令,從硬件層面來看這個memroy barrier就是CPU flush store bufferes中的指令。軟件層面能夠決定在適當的地方來插入內存屏障。
CPU層面的內存屏障
什麼是內存屏障?從前面的內容基本能有一個初步的猜測,內存屏障就是將store bufferes中的指令寫入到內存,從而使得其餘訪問同一共享內存的線程的可見性。
X86的memory barrier指令包括lfence(讀屏障) sfence(寫屏障) mfence(全屏障)
Store Memory Barrier(寫屏障) 告訴處理器在寫屏障以前的全部已經存儲在存儲緩存(store bufferes)中的數據同步到主內存,簡單來講就是使得寫屏障以前的指令的結果對屏障以後的讀或者寫是可見的
Load Memory Barrier(讀屏障) 處理器在讀屏障以後的讀操做,都在讀屏障以後執行。配合寫屏障,使得寫屏障以前的內存更新對於讀屏障以後的讀操做是可見的
Full Memory Barrier(全屏障) 確保屏障前的內存讀寫操做的結果提交到內存以後,再執行屏障後的讀寫操做
有了內存屏障之後,對於上面這個例子,咱們能夠這麼來改,從而避免出現可見性問題
總的來講,內存屏障的做用能夠經過防止CPU對內存的亂序訪問來保證共享數據在多線程並行執行下的可見性
可是這個屏障怎麼來加呢?回到最開始咱們講volatile關鍵字的代碼,這個關鍵字會生成一個Lock的彙編指令,這個指令其實就至關於實現了一種內存屏障
這個時候問題又來了,內存屏障、重排序這些東西好像是和平臺以及硬件架構有關係的。做爲Java語言的特性,一次編寫多處運行。咱們不該該考慮平臺相關的問題,而且這些所謂的內存屏障也不該該讓程序員來關心。
什麼是JMM
JMM全稱是JavaMemory Model.什麼是JMM呢?
經過前面的分析發現,致使可見性問題的根本緣由是緩存以及重排序。而JMM實際上就是提供了合理的禁用緩存以及禁止重排序的方法。因此它最核心的價值在於解決可見性和有序性。
JMM屬於語言級別的抽象內存模型,能夠簡單理解爲對硬件模型的抽象,它定義了共享內存中多線程程序讀寫操做的行爲規範:在虛擬機中把共享變量存儲到內存以及從內存中取出共享變量的底層實現細節
經過這些規則來規範對內存的讀寫操做從而保證指令的正確性,它解決了CPU多級緩存、處理器優化、指令重排序致使的內存訪問問題,保證了併發場景下的可見性。
須要注意的是,JMM並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序,也就是說在JMM中,也會存在緩存一致性問題和指令重排序問題。只是JMM把底層的問題抽象到JVM層面,再基於CPU層面提供的內存屏障指令,以及限制編譯器的重排序來解決併發問題
JMM抽象模型分爲主內存、工做內存;主內存是全部線程共享的,通常是實例對象、靜態字段、數組對象等存儲在堆內存中的變量。工做內存是每一個線程獨佔的,線程對變量的全部操做都必須在工做內存中進行,不能直接讀寫主內存中的變量,線程之間的共享變量值的傳遞都是基於主內存來完成
Java內存模型底層實現能夠簡單的認爲:經過內存屏障(memory barrier)禁止重排序,即時編譯器根據具體的底層體系架構,將這些內存屏障替換成具體的CPU 指令。對於編譯器而言,內存屏障將限制它所能作的重排序優化。而對於處理器而言,內存屏障將會致使緩存的刷新操做。好比,對於volatile,編譯器將在volatile字段的讀寫操做先後各插入一些內存屏障。
JMM是如何解決可見性有序性問題的
簡單來講,JMM提供了一些禁用緩存以及進制重排序的方法,來解決可見性和有序性問題。這些方法你們都很熟悉:volatile、synchronized、final;
JMM如何解決順序一致性問題
重排序問題
爲了提升程序的執行性能,編譯器和處理器都會對指令作重排序,其中處理器的重排序在前面已經分析過了。所謂的重排序其實就是指執行的指令順序。
編譯器的重排序指的是程序編寫的指令在編譯以後,指令可能會產生重排序來優化程序的執行性能。
編譯器的重排序,JMM提供了禁止特定類型的編譯器重排序。
處理器重排序,JMM會要求編譯器生成指令時,會插入內
存屏障來禁止處理器重排序
固然並非全部的程序都會出現重排序問題
編譯器的重排序和CPU的重排序的原則同樣,會遵照數據依賴性原則,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序,好比下面的代碼,
a=1; b=a;
a=1;a=2;
a=b;b=1;
這三種狀況在單線程裏面若是改變代碼的執行順序,都會致使結果不一致,因此重排序不會對這類的指令作優化。這種規則也成爲as-if-serial。無論怎麼重排序,對於單個線程來講執行結果不能改變。好比
int a=2;//1
int b=3;//2
int rs=a*b; //3
1和三、2和3存在數據依賴,因此在最終執行的指令中,3不能重排序到1和2以前,不然程序會報錯。因爲1和2不存在數據依賴,因此能夠從新排列1和2的順序
JMM層面的內存屏障
爲了保證內存可見性,Java編譯器在生成指令序列的適當
位置會插入內存屏障來禁止特定類型的處理器的重排序,在JMM中把內存屏障分爲四類
HappenBefore
它的意思表示的是前一個操做的結果對於後續操做是可見的,因此它是一種表達多個線程之間對於內存的可見性。因此咱們能夠認爲在JMM中,若是一個操做執行的結果須要對另外一個操做課件,那麼這兩個操做必需要存在happens-before關係。這兩個操做能夠是同一個線程,也能夠是不一樣的線程
JMM中有哪些方法創建happen-before規則
程序順序規則
1. 一個線程中的每一個操做,happens-before於該線程中的任意後續操做;能夠簡單認爲是as-if-serial。單個線程
中的代碼順序無論怎麼變,對於結果來講是不變的
2. volatile變量規則,對於volatile修飾的變量的寫的操做,必定happen-before後續對於volatile變量的讀操做;
根據volatile規則,2happens before 3
3. 傳遞性規則,若是1 happens-before 2; 3happens-before 4; 那麼傳遞性規則表示:1 happens-before 4;
4. start規則,若是線程A執行操做ThreadB.start(),那麼線程A的ThreadB.start()操做happens-before線程B中
的任意操做
5. join規則,若是線程A執行操做ThreadB.join()併成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。
Thread t 1 = new // 此處對共享變量 x 修改 x = 100 }); // 例如此處對共享變量修改, // 則這個修改結果對線程 t1 可見 // 主線程啓動子線程 t1.start(); t1.join(); // 子線程全部對共享變量的修改 // 在主線程調用 t1 .join() 以後皆可見 // 此例中, x = 100
6. 監視器鎖的規則,對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖
synchronized (this) { // 此處自動加鎖 // x 是共享變量 , 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此處自動解鎖
假設x 的初始值是10,線程A 執行完代碼塊後x 的值會變成12(執行完自動釋放鎖),線程B 進入代碼塊時,可以看到線程A 對x 的寫操做,也就是線程B 可以看到x==12。