Java多線程之volatile

做者 某人Valar
如需轉載請保留原文連接html

部分圖片來自百度,若有侵權請聯繫刪除java

相關推薦:c++

Java多線程之Synchronizedgit

Java多線程之volatilegithub

目錄:面試

  • 什麼是volatile,使用時該注意什麼?
  • volatile和原子性、可見性和有序性之間的關係
  • volatile的實現原理(內存屏障、CPU緩存、MESI協議)
  • volatile與synchronized的區別

1. 什麼是volatile?

volatile中文意爲揮發物,不穩定的。在Java中也是一個關鍵字,用於修飾變量。算法

  1. 在JMM(Java Memory Model,Java內存模型)中,有main memory,每一個線程也有本身的memory (例如寄存器)。爲了性能,一個線程會在本身的memory中保持要訪問的變量的副本。緩存

  2. 這樣就會出現同一個變量在某個瞬間,在一個線程的memory中的值可能與另一個線程memory中的值,或者main memory中的值不一致的狀況。安全

  3. 一個變量聲明爲volatile,就意味着這個變量是隨時會被其餘線程修改,線程在每次使用變量的時候,都會讀取變量修改後的最新值bash

Java內存模型圖:

volatile的使用時注意:

  1. volatile不管是修飾實例變量仍是靜態變量,都須要放在數據類型關鍵字以前,即放在Stringint等以前。
  2. volatilefinal不能同時修飾一個變量。volatile 是保證變量被寫時其結果其餘線程可見,而final已經讓該變量不能被再次寫了。

2. volatile和原子性、可見性和有序性之間的關係

關於原子性、可見性和有序性的介紹,以前的一篇文章有了介紹,傳送門

2.1 volatile能夠保證原子性嗎?

不能

  • 咱們知道,原子性是指一個操做不可再被分隔成多步。一個操做或者多個操做 要麼所有執行且執行的過程不會被任何因素打斷,要麼就都不執行。
  • 對於volatile來講,若是一個操做自己不是原子性的,那麼使用volatile做用於這個操做上的一個變量,其也是沒法保證原子性的。

例如咱們常碰到的i++的問題。

i = 1; //原子性操做,不用使用volatile也不會出現線程安全問題。
複製代碼
volatile int i = 0;
i++; //非原子性操做
複製代碼

若是咱們開啓200個線程併發執行i++這行代碼,每一個線程中只執行一遍。若是volatile能夠保證原子性的話,那麼i的最終結果應該是200;而實際上咱們發現這個值是會小於200的,緣由是什麼呢?

// i++ 其能夠被拆解爲
一、線程讀取i
二、temp = i + 1
三、i = temp
複製代碼
  1. 例如當 i=5 的時候A,B兩個線程同時讀入了 i 的值
  2. 而後A線程執行了 temp = i + 1的操做, 要注意,此時的 i 的值尚未變化,而後B線程也執行了temp = i + 1的操做,注意,此時A,B兩個線程保存的 i 的值都是5,temp 的值都是6
  3. 而後A線程執行了 i = temp (6)的操做,此時i的值會當即刷新到主存並通知其餘線程保存的 i 值失效, 此時B線程須要從新讀取 i 的值那麼此時B線程保存的 i 就是6
  4. 同時B線程保存的 temp 還仍然是6, 而後B線程執行 i=temp (6),因此致使了計算結果比預期少了1。

參考:www.cnblogs.com/simpleDi/p/…

那麼如何保證i++這種操做的線程安全呢?

  1. 使用synchronized關鍵字或者Lock。至於爲何,能夠看下synchronized與原子性
synchronized(object){
    i++;
}
複製代碼
  1. 使用支持原子性操做的類,如 java.util.concurrent.atomic.AtomicInteger,它使用的是CAS(compare and swap,比較並替換)算法,效率優於第 1 種。

2.2 volatile與可見性

volatile關鍵字的變量寫操做時,強制緩存和主存同步,其餘線程讀時候發現緩存失效,就去讀主存,由此保證了變量的可見性。

2.3 volatile與有序性

volatile能夠禁止指令重排序,因此說其是能夠保證有序性的。

什麼是指令重排序(Instruction Reorder)?

在Java內存模型中,容許編譯器和處理器對指令進行重排序,重排序的結果不會影響到單線程的執行,但不能保證多線程併發執行時不受影響。

例如如下代碼在未發生指令重排序時,其執行順序爲1->2->3->4。但在真正執行時,將可能變爲1->2->4->3或者2->1->3->4或者其餘。但其會保證1處於3以前,2處於4以前。全部最終結果都是a=10; b=20

int a = 0;//語句1
int b = 1;//語句2
a = 10; //語句3
b = 20; //語句4
複製代碼

但若是是多線程狀況下,另外一個線程中有如下程序。當上述的執行順序被重排序爲1->2->4->3,當線程1執行到第3步b=20時,切換到線程2執行,其會輸出a此時已是10了,而此時a的值其實仍是爲0。

if(b == 20){
  System.out.print("a此時已是10了");
}

複製代碼

3. volatile的實現原理

3.1 內存屏障與指令重排序

  • 要知道volatile是如何禁止指令重排序的,首先須要瞭解一個概念內存屏障

內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,其是一種CPU指令,因此像Java、c++、c語言都有此概念。

  • 它使得 CPU 或編譯器在對內存進行操做的時候, 嚴格按照必定的順序來執行, 也就是說在memory barrier 以前的指令和memory barrier以後的指令不會因爲系統優化等緣由而致使亂序。

A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a CPU or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier. ——— 維基百科

3.1.1 JVM中的4中內存屏障
  1. LoadLoad屏障:
//抽象場景:
Load1; 
LoadLoad; 
Load2
複製代碼

Load1 和 Load2 表明兩條讀取指令。在Load2要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。

  1. StoreStore屏障:
//抽象場景:
Store1; 
StoreStore; 
Store2
複製代碼

Store1 和 Store2表明兩條寫入指令。在Store2寫入執行前,保證Store1的寫入操做對其它處理器可見

  1. LoadStore屏障:
//抽象場景:
Load1; 
LoadStore; 
Store2
複製代碼

在Store2被寫入前,保證Load1要讀取的數據被讀取完畢。

  1. StoreLoad屏障:
//抽象場景:
Store1; 
StoreLoad; 
Load2
複製代碼

在Load2讀取操做執行前,保證Store1的寫入對全部處理器可見。StoreLoad屏障的開銷是四種屏障中最大的。

3.1.2 volatile與內存屏障的關係

在一個變量被volatile修飾後,JVM會爲咱們作兩件事:

  1. 在每一個volatile寫操做前插入StoreStore屏障,在寫操做後插入StoreLoad屏障。
  2. 在每一個volatile讀操做前插入LoadLoad屏障,在讀操做後插入LoadStore屏障。

仍是使用上面的例子:

此次使用volatile修飾變量b

int a = 0;//語句1
volatile int b = 1;//語句2

//在線程1中執行的語句
a = 10; //語句3
b = 20; //語句4

//在線程2中執行的語句
if(b == 20){
   System.out.print("a此時已是10了");
}
複製代碼

在編譯以後線程1中的語句將相似於

a = 10; //語句3
 ----------- StoreStore屏障 ---------------
 b = 20; //語句4
 ----------- StoreLoad屏障 ---------------
複製代碼

因爲屏障的存在,語句3語句4將沒法被指令重排序,從而能夠保證在b=20時,a已經被賦值爲10了。那麼這個程序也就不存在線程安全問題了。

3.1.3 內存屏障的性能影響

內存屏障阻礙了CPU採用優化技術來下降內存操做延遲,必須考慮所以帶來的性能損失。爲了達到最佳性能,最好是把要解決的問題模塊化,這樣處理器能夠按單元執行任務,而後在任務單元的邊界放上全部須要的內存屏障。採用這個方法可讓處理器不受限的執行一個任務單元。

參考:ifeve.com/memory-barr…

3.2 volatile如何實現可見性?

要知道volatile是如何保證可見性的須要先了解下有關CPU緩存的概念。

3.2.1 CPU緩存

咱們知道CPU的運算速度要比內存的讀寫速度快不少,這就形成了內存沒法跟上CPU的狀況,由此出現了CPU緩存。其是CPU與內存之間的臨時數據交換器,咱們常見的CPU會有3級緩存,常稱爲L一、L二、L3。

下圖是Intel Core i7處理器的高速緩存概念模型(圖片來自《深刻理解計算機系統》)

當系統運行時,CPU執行計算的過程以下:

  1. 程序以及數據被加載到主內存
  2. 指令和數據被加載到CPU緩存
  3. CPU執行指令,把結果寫到高速緩存
  4. 高速緩存中的數據寫回主內存

在上述的緩存模型下,當多核併發執行某項任務時就容易出現問題。eg.

  1. 核0先從內存中讀取了變量a。
  2. 核3也從內存中讀取了變量a。
  3. 核0修改了變量a,並同步到了主內存中。
  4. 核3開始使用變量a,但值仍然是舊的。

爲了解決這類問題,出現了針對CPU的MESI協議

3.2.2 MESI協議

在早期的CPU中,是經過在總線加LOCK#鎖的方式實現的(又稱總線鎖)。當一個CPU對其緩存中的數據進行操做的時候,往總線中發送一個Lock信號。 這個時候,全部CPU收到這個信號以後就不操做本身緩存中的對應數據了,當操做結束,釋放鎖之後,全部的CPU就去內存中獲取最新數據更新。

但這種方式開銷太大,因此Intel開發了緩存一致性協議,也就是MESI協議。它的方法是在CPU緩存中保存一個標記位,這個標記位有四種狀態:

  • M: Modify,修改緩存,當前CPU的緩存已經被修改了,即與內存中數據已經不一致了;
  • E: Exclusive,獨佔緩存,當前CPU的緩存和內存中數據保持一致,並且其餘處理器並無可以使用的緩存數據;
  • S: Share,共享緩存,和內存保持一致的一份拷貝,多組緩存能夠同時擁有針對同一內存地址的共享緩存段;
  • I: Invalid,無效緩存,這個說明CPU中的緩存已經不能使用了。

CPU的讀取遵循下面幾點:

  • 若是緩存狀態是I,那麼就從內存中讀取,不然就從緩存中直接讀取。
  • 若是緩存處於M或E的CPU讀取到其餘CPU有讀操做,就把本身的緩存寫入到內存中,並將本身的狀態設置爲S。
  • 只有緩存狀態是M或E的時候,CPU才能夠修改緩存中的數據,修改後,緩存狀態變爲M。

舉個常見的例子就是:

當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,那麼他會發出信號通知其餘CPU將該變量的緩存行設置爲無效狀態。當其餘CPU使用這個變量時,首先會去嗅探是否有對該變量更改的信號,當發現這個變量的緩存行已經無效時,會重新從內存中讀取這個變量。

3.2.3 volatile的可見性原理

瞭解了上面的內容,就能夠很容易的理解volatile是如何實現的了。

  1. 首先被volatile關鍵字修飾的共享變量在轉換成彙編語言時,會加上一個以lock爲前綴的指令
  2. 當CPU發現這個指令時,當即作兩件事:
      1. 將當前內核高速緩存行的數據馬上回寫到內存
      1. 經過MESI協議使在其餘內核裏緩存了的數據無效,這樣其餘線程也必須從內存中從新讀取數據了。

參考:

crowhawk.github.io/2018/02/10/… blog.csdn.net/nch_ren/art…

4. volatile與synchronized的區別

volatile到此也介紹的很多了,最後來講下其與synchronized的區別。

瞭解更多synchronized的相關內容,請戳這裏

  1. volatile是變量修飾符,而synchronized則做用於一段代碼或方法。
  2. volatile只是在線程內存和「主」內存間同步某個變量的值;而synchronized經過鎖定和解鎖某個監視器同步全部變量的值。顯然synchronized要比volatile消耗更多資源。
  3. volatile不能保證原子性,能夠保證可見性、有序性(靠內存屏障實現)。synchronized能夠保證原子性、可見性、有序性。當你和麪試官說到這裏時,你最好清楚裏面的具體細節,例如是從何種角度來看的有序性,以及如何實現的該特性,否則面試官很容易被問住的。

結語

至此關於volatile的內容到這裏就結束了,若是文中有錯誤的地方、或者有其餘關於volatile比較重要的內容又沒有介紹到的,歡迎在評論區裏留言,一塊兒交流學習。

相關文章
相關標籤/搜索