volatile 關鍵字詳解

volatile 關鍵字詳解

volatile 關鍵字詳解

01

概念

1 volatile變量,用來確保將變量的更新操做通知到其餘線程。
2 當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,所以不會將該變量上的操做與其餘內存操做一塊兒重排序。
3 volatile變量不會被緩存在寄存器或者對其餘處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新寫入的值。java

02

特性

假如一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,具有如下特性:
一、保證多線程下的可見性   
二、對於單個的共享變量的讀/寫具備原子性,沒法保證相似num++的原子性。
三、禁止進行指令重排序(即保證有序性)。即volatile前面的代碼先於後面的代碼先執行面試

在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。在Java裏面,能夠經過volatile關鍵字來保證必定的「有序性」。
Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,也就是happens-before 原則。redis

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

Java內存模型 (Java Memory Model,JMM)
Java內存模型由Java虛擬機規範定義,用來屏蔽各個平臺的硬件差別。簡單來講:
1 全部變量儲存在主內存。2 每條線程擁有本身的工做內存,其中保存了主內存中線程使用到的變量的副本。3 線程不能直接讀寫主內存中的變量,全部操做均在工做內存中完成。設計模式

線程,主內存,工做內存的交互關係如圖:
volatile 關鍵字詳解緩存

和volatile有關的操做爲微信

read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用markdown

load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。

03

爲何要使用Volatile

Volatile變量修飾符若是使用恰當的話,它比synchronized的使用和執行成本會更低,由於它不會引發線程上下文的切換和調度。多線程

03

volatile的原子性問題

volatile僅僅保障對其修飾的變量的寫操做( 以及讀操做 )自己的原子性 ,而這並不表示對 volatile 變量的賦值操做必定具備原子性。例如,以下對volatile 變量 count1的賦值操做並非原子操做:
count1 = count2 + 1;
若是變量count2也是一個共享變量,那麼該賦值操做其實是一個read-modify-write 操做。其執行過程當中其餘線程可能已經更新了 count2 的值,所以該操做不具有不可分割性,也就不是原子操做。若是變量count2 是一個局部變量,那麼該賦值操做就是一個原子操做。
對volatile變量的賦值操做,其右邊表達式中只要涉及共享變量 ( 包括被賦值的 volatile 變量自己 ),那麼這個賦值操做就不是原子操做。要保障這樣操做的原子性, 仍然須要藉助鎖。併發

04

解決num++操做的原子性問題

針對num++這類複合類的操做,可使用java併發包中的原子操做類原子操做類是經過循環CAS的方式來保證其原子性的。

public class Counter {  //使用原子操做類
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch來等待計算線程執行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
   //開啓30個線程進行累加操做
   for(int i=0;i<30;i++){
       new Thread(){
           public void run(){
               for(int j=0;j<10000;j++){
                   num.incrementAndGet();//原子性的num++,經過循環CAS方式
               }
               countDownLatch.countDown();
           }
       }.start();
   }
   //等待計算線程執行完
   countDownLatch.await();
   System.out.println(num);
}
}

05

實現原理

可見性實現原理
將一個共享變量聲明爲volatile後,會有如下效應
1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
2.這個寫會操做會致使其餘線程中的緩存無效。
volatile可以保證可見性,那麼它是如何實現可見性的呢?以X86處理器爲例,在對volatile修飾的變量進行寫操做時,經過編譯器生成反彙編指令後,會發現會多一條Lock前綴,就是因爲這條Lock前綴所實現的可見性。Lock前綴在多核處理器中會引起下面這兩件事情:
1Lock指令會將當前處理器緩存行的數據寫回到主內存。(ps:每一個處理器都有本身的cache緩存,每次緩存中操做的變量都是主內存中變量的拷貝) 2 一個處理器寫回主內存的操做會形成其餘處理的緩存無效。

禁止指令重排原理
經過內存屏障來實現禁止指令重排。
如圖
volatile 關鍵字詳解

06

volatile的使用優化

在JDK7的併發包裏新增了一個隊列集合類LinkedTransferQueue,它在使用volatile變量的時候,會採用一種將字節追加到64字節的方法來提升性能。
追加到64字節可以優化性能緣由
在不少處理器中它們的L一、L二、L3緩存的高速緩存行都是64字節寬,不支持填充緩存行,例如,如今有兩個不足64字節的變量AB,那麼在AB變量寫入緩存行時會將AB變量的部分數據一塊兒寫入一個緩存行中,那麼在CPU1和CPU2想同時訪問AB變量時是沒法實現的,也就是想同時訪問一個緩存行的時候會引發衝突,若是能夠填充到64字節,AB兩個變量會分別寫入到兩個緩存行中,這樣就能夠併發,同時進行變量訪問,從而提升效率。

07

總結

volatile是一種輕量級的同步機制,它主要有三個特性:
一是保證共享變量對全部線程的可見性
二是禁止指令重排序優化
三是volatile對於單個的共享變量的讀/寫具備原子性,沒法保證相似num++的原子性,須要經過循環CAS的方式來保證num++操做的原子性。

推薦閱讀:

  • 深刻解析HashMap和ConcurrentHashMap源碼以及底層原理

  • 設計模式(二):幾種工廠模式詳解

  • 進程同步的五種機制以及優缺點(翻譯)

  • redis五種數據類型的實現方式,經常使用命令,應用場景

  • redis和memcahed的共同點,區別以及應用場景

  • 詳解TCP的三次握手與四次揮手及面試題(很全面)

  • Arrays 工具類詳解(超詳細)

  • 算法必須掌握幾種方法

  • QPS、TPS、併發用戶數、吞吐量

  • 設計模式之單例模式

  • Collections 工具類詳解(超詳細)

END
volatile 關鍵字詳解
掃描二維碼 | 關注咱們
微信公衆號 : jiagoudiantang
CSDN : https://fking.blog.csdn.net

相關文章
相關標籤/搜索