淺談 volatile 實現原理

synchronized 是一個重量級的鎖,的 volatile 則是輕量級的 synchronized ,它在多線程開發中保證了共享變量的「可見性」。若是一個變量使用 volatile ,則它比使用 synchronized 的成本更加低,由於它不會引發線程上下文的切換和調度。數據庫

Java 編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保經過排他鎖單獨得到這個變量。 通俗點講就是說一個變量若是用 volatile 修飾了,則 Java 能夠確保全部線程看到這個變量的值是一致的。若是某個線程對 volatile 修飾的共享變量進行更新,那麼其餘線程能夠立馬看到這個更新,這就是所謂的線程可見性。編程

內存模型相關概念

理解 volatile 其實仍是有點兒難度的,它與 Java 的內存模型有關,因此在理解 volatile 以前咱們須要先了解有關 Java 內存模型的概念。緩存

操做系統語義

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

有了 CPU 高速緩存雖然解決了效率問題,可是它會帶來一個新的問題:數據一致性。併發

在程序運行中,會將運行所須要的數據複製一份到 CPU 高速緩存中,在進行運算時 CPU 再也不也主存打交道,而是直接從高速緩存中讀寫數據,只有當運行結束後,纔會將數據刷新到主存中。app

舉一個簡單的例子:編程語言

i = i + 1;
複製代碼

當線程運行這段代碼時,首先會從主存中讀取 i 的值( 假設此時 i = 1 ),而後複製一份到 CPU 高速緩存中,而後 CPU 執行 + 1 的操做(此時 i = 2),而後將數據 i = 2 寫入到告訴緩存中,最後刷新到主存中。性能

其實這樣作在單線程中是沒有問題的,有問題的是在多線程中。以下:spa

假若有兩個線程 A、B 都執行這個操做( i++ ),
複製代碼

按照咱們正常的邏輯思惟主存中的i值應該=3 。操作系統

但事實是這樣麼?分析以下:

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

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

經過在總線加 LOCK# 鎖的方式
經過緩存一致性協議
複製代碼

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

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

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. 同 <3> 同樣

那麼 64 位的 JDK 環境下,對 64 位數據的讀寫是不是原子的呢?

實現對普通long與double的讀寫不要求是原子的(但若是實現爲原子操做也OK)
實現對volatile long與volatile double的讀寫必須是原子的(沒有選擇餘地)
複製代碼

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

可見性

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

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

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

當一個變量被 volatile 修飾後,表示着線程本地內存無效。

當一個線程修改共享變量後他會當即被更新到主內存中;

當其餘線程讀取共享變量時,它會直接從主內存中讀取。

synchronize 和鎖均可以保證可見性。

有序性

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

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

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

剖析 volatile 原理

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

上面那段話,有兩層語義:

保證可見性、不保證原子性
禁止指令重排序
複製代碼

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

指令重排序

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

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

指令重排序對單線程沒有什麼影響,他不會影響程序的運行結果,可是會影響多線程的正確性。既然指令重排序會影響到多線程執行的正確性,那麼咱們就須要禁止重排序。那麼JVM是如何禁止重排序的呢?

由此引出happen-before原則

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做,happens-before 於書寫在後面的操做。
  2. 鎖定規則:一個 unLock 操做,happens-before 於後面對同一個鎖的 lock 操做。
  3. volatile 變量規則:對一個volatile變量的寫操做,happens-before 於後面對這個變量的讀操做。注意是後面的.
  4. 傳遞規則:若是操做 A happens-before 操做 B,而操做 B happens-before 操做C,則能夠得出,操做 A happens-before 操做C
  5. 線程啓動規則:Thread 對象的 start 方法,happens-before 此線程的每一個一個動做。
  6. 線程中斷規則:對線程 interrupt 方法的調用,happens-before 被中斷線程的代碼檢測到中斷事件的發生。
  7. 線程終結規則:線程中全部的操做,都 happens-before 線程的終止檢測,咱們能夠經過Thread.join() 方法結束、Thread.isAlive() 的返回值手段,檢測到線程已經終止執行。
  8. 對象終結規則:一個對象的初始化完成,happens-before 它的 finalize() 方法的開始

咱們着重看第三點 Volatile規則:對 volatile變量的寫操做,happen-before 後續的讀操做。

爲了實現 volatile 內存語義,JMM會重排序,其規則以下:

當第二個操做是 volatile 寫操做時,無論第一個操做是什麼,都不能重排序。
這個規則,確保 volatile 寫操做以前的操做,都不會被編譯器重排序到 volatile 寫操做以後。
複製代碼

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

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

下圖是完成上述規則所須要的內存屏障:

總結

volatile 看起來簡單,可是要想理解它仍是比較難的,這裏只是對其進行基本的瞭解。

volatile 相對於 synchronized 稍微輕量些,在某些場合它能夠替代 synchronized ,可是又不能徹底取代 synchronized 。只有在某些場合纔可以使用 volatile,使用它必須知足以下兩個條件:

對變量的寫操做,不依賴當前值。
該變量沒有包含在具備其餘變量的不變式中。
複製代碼

volatile 常常用於如下場景:狀態標記變量、Double Check .一個線程寫多個線程讀。

相關文章
相關標籤/搜索