【死磕Java併發】-----深刻分析volatile的實現原理

 

經過前面一章咱們瞭解了synchronized是一個重量級的鎖,雖然JVM對它作了不少優化,而下面介紹的volatile則是輕量級的synchronized。若是一個變量使用volatile,則它比使用synchronized的成本更加低,由於它不會引發線程上下文的切換和調度。Java語言規範對volatile的定義以下:html

Java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保經過排他鎖單獨得到這個變量。java

上面比較繞口,通俗點講就是說一個變量若是用volatile修飾了,則Java能夠確保全部線程看到這個變量的值是一致的,若是某個線程對volatile修飾的共享變量進行更新,那麼其餘線程能夠立馬看到這個更新,這就是所謂的線程可見性。數據庫

volatile雖然看起來比較簡單,使用起來無非就是在一個變量前面加上volatile便可,可是要用好並不容易(LZ認可我至今仍然使用很差,在使用時仍然是模棱兩可)。編程

內存模型相關概念

理解volatile其實仍是有點兒難度的,它與Java的內存模型有關,因此在理解volatile以前咱們須要先了解有關Java內存模型的概念,這裏只作初步的介紹,後續LZ會詳細介紹Java內存模型。緩存

操做系統語義

計算機在運行程序時,每條指令都是在CPU中執行的,在執行過程當中勢必會涉及到數據的讀寫。咱們知道程序運行的數據是存儲在主存中,這時就會有一個問題,讀寫主存中的數據沒有CPU中執行指令的速度快,若是任何的交互都須要與主存打交道則會大大影響效率,因此就有了CPU高速緩存。CPU高速緩存爲某個CPU獨有,只與在該CPU運行的線程有關。多線程

有了CPU高速緩存雖然解決了效率問題,可是它會帶來一個新的問題:數據一致性。在程序運行中,會將運行所須要的數據複製一份到CPU高速緩存中,在進行運算時CPU再也不也主存打交道,而是直接從高速緩存中讀寫數據,只有當運行結束後纔會將數據刷新到主存中。舉一個簡單的例子:併發

i++i++

當線程運行這段代碼時,首先會從主存中讀取i( i = 1),而後複製一份到CPU高速緩存中,而後CPU執行 + 1 (2)的操做,而後將數據(2)寫入到告訴緩存中,最後刷新到主存中。其實這樣作在單線程中是沒有問題的,有問題的是在多線程中。以下:app

假若有兩個線程A、B都執行這個操做(i++),按照咱們正常的邏輯思惟主存中的i值應該=3,但事實是這樣麼?分析以下:編程語言

兩個線程從主存中讀取i的值(1)到各自的高速緩存中,而後線程A執行+1操做並將結果寫入高速緩存中,最後寫入主存中,此時主存i==2,線程B作一樣的操做,主存中的i仍然=2。因此最終結果爲2並非3。這種現象就是緩存一致性問題。性能

解決緩存一致性方案有兩種:

  1. 經過在總線加LOCK#鎖的方式
  2. 經過緩存一致性協議

可是方案1存在一個問題,它是採用一種獨佔的方式來實現的,即總線加LOCK#鎖的話,只能有一個CPU可以運行,其餘CPU都得阻塞,效率較爲低下。

第二種方案,緩存一致性協議(MESI協議)它確保每一個緩存中使用的共享變量的副本是一致的。其核心思想以下:當某個CPU在寫數據時,若是發現操做的變量是共享變量,則會通知其餘CPU告知該變量的緩存行是無效的,所以其餘CPU在讀取該變量時,發現其無效會從新從主存中加載數據。

212219343783699

Java內存模型

上面從操做系統層次闡述瞭如何保證數據一致性,下面咱們來看一下Java內存模型,稍微研究一下Java內存模型爲咱們提供了哪些保證以及在Java中提供了哪些方法和機制來讓咱們在進行多線程編程時可以保證程序執行的正確性。

在併發編程中咱們通常都會遇到這三個基本概念:原子性、可見性、有序性。咱們稍微看下volatile

原子性

原子性:即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。

原子性就像數據庫裏面的事務同樣,他們是一個團隊,同生共死。其實理解原子性很是簡單,咱們看下面一個簡單的例子便可:

i = 0;            ---1
j = i ;            ---2
i++;            ---3
i = j + 1;    ---4

上面四個操做,有哪一個幾個是原子操做,那幾個不是?若是不是很理解,可能會認爲都是原子性操做,其實只有1纔是原子操做,其他均不是。

1—在Java中,對基本數據類型的變量和賦值操做都是原子性操做;
2—包含了兩個操做:讀取i,將i值賦值給j
3—包含了三個操做:讀取i值、i + 1 、將+1結果賦值給i;
4—同三同樣

在單線程環境下咱們能夠認爲整個步驟都是原子性操做,可是在多線程環境下則不一樣,Java只保證了基本數據類型的變量和賦值操做纔是原子性的(注:在32位的JDK環境下,對64位數據的讀取不是原子性操做*,如long、double)。要想在多線程環境下保證原子性,則能夠經過鎖、synchronized來確保。

volatile是沒法保證複合操做的原子性

可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。

在上面已經分析了,在多線程環境下,一個線程對共享變量的操做對其餘線程是不可見的。

Java提供了volatile來保證可見性。

當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會當即被更新到主內存中,當其餘線程讀取共享變量時,它會直接從主內存中讀取。
固然,synchronize和鎖均可以保證可見性。

有序性

有序性:即程序執行的順序按照代碼的前後順序執行。

在Java內存模型中,爲了效率是容許編譯器和處理器對指令進行重排序,固然重排序它不會影響單線程的運行結果,可是對多線程會有影響。

Java提供volatile來保證必定的有序性。最著名的例子就是單例模式裏面的DCL(雙重檢查鎖)。這裏LZ就再也不闡述了。

剖析volatile原理

JMM比較龐大,不是上面一點點就可以闡述的。上面簡單地介紹都是爲了volatile作鋪墊的。

volatile能夠保證線程可見性且提供了必定的有序性,可是沒法保證原子性。在JVM底層volatile是採用「內存屏障」來實現的。

上面那段話,有兩層語義

  1. 保證可見性、不保證原子性
  2. 禁止指令重排序

第一層語義就不作介紹了,下面重點介紹指令重排序。

在執行程序時爲了提升性能,編譯器和處理器一般會對指令作重排序:

  1. 編譯器重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序;
  2. 處理器重排序。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序;

指令重排序對單線程沒有什麼影響,他不會影響程序的運行結果,可是會影響多線程的正確性。既然指令重排序會影響到多線程執行的正確性,那麼咱們就須要禁止重排序。那麼JVM是如何禁止重排序的呢?這個問題稍後回答,咱們先看另外一個原則happens-before,happen-before原則保證了程序的「有序性」,它規定若是兩個操做的執行順序沒法從happens-before原則中推到出來,那麼他們就不能保證有序性,能夠隨意進行重排序。其定義以下:

  1. 同一個線程中的,前面的操做 happen-before 後續的操做。(即單線程內按代碼順序執行。可是,在不影響在單線程環境執行結果的前提下,編譯器和處理器能夠進行重排序,這是合法的。換句話說,這一是規則沒法保證編譯重排和指令重排)。
  2. 監視器上的解鎖操做 happen-before 其後續的加鎖操做。(Synchronized 規則)
  3. 對volatile變量的寫操做 happen-before 後續的讀操做。(volatile 規則)
  4. 線程的start() 方法 happen-before 該線程全部的後續操做。(線程啓動規則)
  5. 線程全部的操做 happen-before 其餘線程在該線程上調用 join 返回成功後的操做。
  6. 若是 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

咱們着重看第三點volatile規則:對volatile變量的寫操做 happen-before 後續的讀操做。爲了實現volatile內存語義,JMM會重排序,其規則以下:

對happen-before原則有了稍微的瞭解,咱們再來回答這個問題JVM是如何禁止重排序的?

20170104-volatile

觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。lock前綴指令其實就至關於一個內存屏障。內存屏障是一組處理指令,用來實現對內存操做的順序限制。volatile的底層就是經過內存屏障來實現的。下圖是完成上述規則所須要的內存屏障:

volatile暫且下分析到這裏,JMM體系較爲龐大,不是三言兩語可以說清楚的,後面會結合JMM再一次對volatile深刻分析。

20170104-volatile2

總結

volatile看起來簡單,可是要想理解它仍是比較難的,這裏只是對其進行基本的瞭解。volatile相對於synchronized稍微輕量些,在某些場合它能夠替代synchronized,可是又不能徹底取代synchronized,只有在某些場合纔可以使用volatile。使用它必須知足以下兩個條件:

  1. 對變量的寫操做不依賴當前值;
  2. 該變量沒有包含在具備其餘變量的不變式中。

volatile常常用於兩個兩個場景:狀態標記兩、double check

參考資料

  1. 周志明:《深刻理解Java虛擬機》
  2. 方騰飛:《Java併發編程的藝術》
  3. Java併發編程:volatile關鍵字解析
  4. Java 併發編程:volatile的使用及其原理
相關文章
相關標籤/搜索