【併發與多線程】Volatile大總結

大佬們的總結文章:
第一篇(偏底層)
第二篇(偏理解)
第三篇(代碼解釋)
(PS:本文基於以上三篇文章)

一.爲何有volatile

首先java內存模型(JMM)中,每一個線程有本身的工做內存,同時還有一個共享的主內存。以下圖:
2.png
不直接讓線程從主內存(MainMemory)中讀取數據,是由於cpu的指令速度遠超內存的存取速度,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝。也就是圖中的工做內存。
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來更高的複雜度,由於它引入了一個新的問題:緩存一致性(CacheCoherence)。以下圖:
1.png
假如說線程1修改了data變量的值爲1,而後將這個修改寫入本身的本地工做內存。那麼此時,線程1的工做內存裏的data值爲1。然而,主內存裏的data值仍是爲0!線程2的工做內存裏的data值也仍是0。html

這就是所謂的 java併發編程中的可見性問題
多個線程併發讀寫一個共享變量的時候,有可能某個線程修改了變量的值,可是其餘線程看不到!也就是對其餘線程不可見!

二.volatile的做用及背後的原理

要解決可見性問題,只須要用volatile修飾data。
以下代碼:java

public class Solution {
  private static volatile int data = 0;
  public static void main(String[] args) {
    DataThread1 d1 = new DataThread1();
    DataThread2 d2 = new DataThread2();
    // 線程1讀取和修改data變量值
    d1.run();
    // 線程2讀取data變量值
    d2.run();
  }
}

這裏說一下volatile的主要功能:編程

  • 第一,一旦data變量定義的時候前面加了volatile來修飾的話,那麼線程1只要修改data變量的值,就會在修改完本身本地工做內存的data變量值以後,強制將這個data變量最新的值刷回主內存,必須讓主內存裏的data變量值立馬變成最新的值!

3.png

  • 第二,若是此時別的線程的工做內存中有這個data變量的本地緩存,也就是一個變量副本的話,那麼會強制讓其餘線程的工做內存中的data變量緩存直接失效過時,不容許再次讀取和使用了!

4.png

  • 第三,若是線程2在代碼運行過程當中再次須要讀取data變量的值,此時嘗試從本地工做內存中讀取,就會發現這個data = 0已通過期了!此時,他就必須從新從主內存中加載data變量最新的值!那麼不就能夠讀取到data = 1這個最新的值了!

5.png

小結:
對一個變量加了volatile關鍵字修飾以後,只要一個線程修改了這個變量的值,立馬強制刷回主內存。
接着強制過時其餘線程的本地工做內存中的緩存,最後其餘線程讀取變量值的時候,強制從新從主內存來加載最新的值!
這樣就保證,任何一個線程修改了變量值,其餘線程立馬就能夠看見了!這就是所謂的volatile保證了可見性的工做原理!緩存

volatile主要做用是保證可見性以及有序性。
有序性涉及到較爲複雜的指令重排內存屏障等概念,本文沒說起,可是volatile是不能保證原子性的安全

  • 也就是說,volatile主要解決的是一個線程修改變量值以後,其餘線程立馬能夠讀到最新的值,是解決這個問題的,也就是可見性!
  • 可是若是是多個線程同時修改一個變量的值,那仍是可能出現多線程併發的安全問題,致使數據值修改錯亂,volatile是不負責解決這個問題的,也就是不負責解決原子性問題!
  • 原子性問題,得依賴synchronized、ReentrantLock等加鎖機制來解決。

JMM主要就是圍繞着如何在併發過程當中如何處理原子性、可見性和有序性這3個特徵來創建的,經過解決這三個問題,能夠解除緩存不一致的問題。而volatile跟可見性和有序性都有關。
(PS:三種特性看第三篇文章)多線程

三.volatile與synchronized區別

  • volatile只能修飾實例變量和類變量,而synchronized能夠修飾方法,以及代碼塊。
  • volatile保證數據的可見性,可是不保證原子性(多線程進行寫操做,不保證線程安全);而synchronized是一種排他(互斥)的機制。
  • volatile用於禁止指令重排序:能夠解決單例雙重檢查對象初始化代碼執行亂序問題。
  • volatile能夠看作是輕量版的synchronized,volatile不保證原子性,可是若是是對一個共享變量進行多個線程的賦值,而沒有其餘的操做,那麼就能夠用volatile來代替synchronized,由於賦值自己是有原子性的,而volatile又保證了可見性,因此就能夠保證線程安全了。

四.volatile的應用

狀態量標記
int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
}
單例模式中的應用
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

五.總結

  1. volatile修飾符適用於如下場景:某個屬性被多個線程共享,其中有一個線程修改了此屬性,其餘線程能夠當即獲得修改後的值,好比boolean flag;或者做爲觸發器,實現輕量級同步。
  2. volatile屬性的讀寫操做都是無鎖的,它不能替代synchronized,由於它沒有提供原子性和互斥性。由於無鎖,不須要花費時間在獲取鎖和釋放鎖上,因此說它是低成本的。
  3. volatile只能做用於屬性,咱們用volatile修飾屬性,這樣compilers就不會對這個屬性作指令重排序。
  4. volatile提供了可見性,任何一個線程對其的修改將立馬對其餘線程可見,volatile屬性不會被線程緩存,始終從主存中讀取。
  5. volatile提供了happens-before保證,對volatile變量v的寫入happens-before全部其餘線程後續對v的讀操做。
  6. volatile能夠使得long和double的賦值是原子的。
  7. volatile能夠在單例雙重檢查中實現可見性和禁止指令重排序,從而保證安全性。
相關文章
相關標籤/搜索