volatile 底層原理以及特性詳解

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

概念

1 volatile變量,用來確保將變量的更新操作通知到其他線程。

2 當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。

3 volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

特性

假如一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,具備以下特性:

1、保證多線程下的可見性

2、對於單個的共享變量的讀/寫具有原子性,無法保證類似num++的原子性。

3、禁止進行指令重排序(即保證有序性)。即volatile前面的代碼先於後面的代碼先執行

在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。在Java裏面,可以通過volatile關鍵字來保證一定的“有序性”。

Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,也就是happens-before 原則。

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有關的操作爲:

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

爲什麼要使用Volatile

Volatile變量修飾符如果使用恰當的話,它比synchronized的使用和執行成本會更低,因爲它不會引起線程上下文的切換和調度。

volatile的原子性問題

volatile僅僅保障對其修飾的變量的寫操作( 以及讀操作 )本身的原子性 ,而這並不表示對 volatile 變量的賦值操作一定具有原子性。例如,如下對volatile 變量 count1的賦值操作並不是原子操作:

count1 = count2 + 1;

如果變量count2也是一個共享變量,那麼該賦值操作實際上是一個read-modify-write 操作。其執行過程中其他線程可能已經更新了 count2 的值,因此該操作不具備不可分割性,也就不是原子操作。如果變量count2 是一個局部變量,那麼該賦值操作就是一個原子操作。

對volatile變量的賦值操作,其右邊表達式中只要涉及共享變量 ( 包括被賦值的 volatile 變量本身 ),那麼這個賦值操作就不是原子操作。要保障這樣操作的原子性, 仍然需要藉助鎖。

解決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);
}
}

實現原理

可見性實現原理

將一個共享變量聲明爲volatile後,會有以下效應

1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;

2.這個寫會操作會導致其他線程中的緩存無效。

volatile能夠保證可見性,那麼它是如何實現可見性的呢?以X86處理器爲例,在對volatile修飾的變量進行寫操作時,通過編譯器生成反彙編指令後,會發現會多一條Lock前綴,就是由於這條Lock前綴所實現的可見性。Lock前綴在多核處理器中會引發下面這兩件事情:

​ 1Lock指令會將當前處理器緩存行的數據寫回到主內存。(ps:每個處理器都有自己的cache緩存,每次緩存中操作的變量都是主內存中變量的拷貝)
​ 2 一個處理器寫回主內存的操作會造成其他處理的緩存無效。

禁止指令重排原理

​ 通過內存屏障來實現禁止指令重排。

如圖
在這裏插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FexTyina-1591706164985)(E:\技術帖子\筆記\基礎\圖\volatile\內存屏障.png)]

volatile的使用優化

在JDK7的併發包裏新增了一個隊列集合類LinkedTransferQueue,它在使用volatile變量的時候,會採用一種將字節追加到64字節的方法來提高性能。

追加到64字節能夠優化性能原因

在很多處理器中它們的L1、L2、L3緩存的高速緩存行都是64字節寬,不支持填充緩存行,例如,現在有兩個不足64字節的變量AB,那麼在AB變量寫入緩存行時會將AB變量的部分數據一起寫入一個緩存行中,那麼在CPU1和CPU2想同時訪問AB變量時是無法實現的,也就是想同時訪問一個緩存行的時候會引起衝突,如果可以填充到64字節,AB兩個變量會分別寫入到兩個緩存行中,這樣就可以併發,同時進行變量訪問,從而提高效率。

總結

volatile是一種輕量級的同步機制,它主要有三個特性:

一是保證共享變量對所有線程的可見性

二是禁止指令重排序優化

三是volatile對於單個的共享變量的讀/寫具有原子性,無法保證類似num++的原子性,需要通過循環CAS的方式來保證num++操作的原子性。

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章