你真的會用volatile嗎

volatile的概念

或者說,volatile解決什麼問題?html

我本身的總結:volatile解決多線程下變量訪問的內存可見性問題,用於線程間通訊。java

通訊怎能理解呢,線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過
主內存向線程B發送消息。c++

java語言標準規範對volatile的描述是這樣的:程序員

The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.編程

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).緩存

上面這段話摘自這個連接,有興趣的能夠本身點開看。多線程

https://docs.oracle.com/javas...架構

大概意思是,java語言容許多個線程訪問共享變量。爲了保證共享變量能準確一致的更新,線程要保證經過鎖的機制單獨得到這個變量。java提供了一種機制,容許你把變量定義成volatile,在某些狀況下比直接用鎖更加方便。併發

若是一個變量被定義成volatile,java內存模型確保全部線程看到的這個共享變量是一致的。oracle

這個一致怎麼理解呢?繼續往下看。

volatile詳解

先來看一幅圖,

在這裏插入圖片描述

這是一幅計算的內存架構圖。

如今的CPU大部分都是多核的,在計算機內部,變量讀寫的流程是這樣的:

  • 當一個處理器須要讀取變量的時候,首先會把變量從主內存讀到緩存,也有多是寄存器,而後再作各類計算。
  • 計算的結果由寄存器刷新到緩存,而後再由緩存刷新到主內存。
  • 一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效,這樣其它處理器

這裏的一個關鍵點是,何時刷新?答案是不知道。咱們不能假設CPU何時會刷新。這樣就會帶來一些問題,好比一個線程寫完一個共享變量,尚未刷新到主內存。而後另外一個線程讀這個變量仍是舊的值,在不少場景下,這個結果和程序員指望的並不一致。

幸運的是,咱們雖然不知道CPU何時刷新,可是咱們能夠強制CPU執行刷新。

再來看一個圖,這是JAVA的內存模型圖。

在這裏插入圖片描述

本地內存是JVM裏一個抽象的概念,它能夠涵蓋寄存器,緩存等。

咱們把這兩幅圖對應起來,能夠這樣解釋。

在JAVA中,當一個線程寫變量時,會先把這個變量從主內存拷貝一份線程的本地內存,而後在本地內存操做。操做完成以後,再刷新到主內存。只有刷新後,另外一個線程才能讀取新的值。

來看個例子:

public class VolatileTest implements Runnable {
    private boolean running = true;

    @Override
    public void run() {
        if (running) {
            System.out.println("I am running");
        }
    }

    public void stop() {
        running = false;
    }
}

這段代碼在多線程環境下執行的時候,假設A線程正在執行run方法,B線程執行了stop方法,咱們的程序無法保證A線程何時會立刻中止。由於這取決於CPU何時進行刷新,把最新變量的值同步到主內存。

解決方法是,把running這個共享變量用volatile修飾便可,這樣能夠保證B線程的修改會馬上刷新到主內存,對其它線程可見。

public class VolatileTest implements Runnable {
    private volatile boolean running = true;

再來看個稍微複雜一點的例子。

public class VolatileTest {
    public volatile int a = 0;
    volatile boolean flag = false;

    public void write() {
        a = 1; // 位置1
        flag = true; //// 位置2
    }
    public void read() {
        if (flag) { // 位置3
            int i = a; // 位置4
        }
    }
}

Java規範對於volatile變量規則是:對一個volatile域的寫,happens-before於任意後續對這個volatile域的
讀。

假設線程A執行writer()方法以後,線程B執行reader()方法。根據volatile變量的happens-before規則,位置2必然先於位置3執行。同時咱們知道在同一個線程中,全部操做必須按照程序的順序來執行,因此位置1確定早於位置2,位置3早於位置4。而後咱們能推出位置1早於位置4。

這樣的順序是符合咱們預期的。

這裏A線程寫一個volatile變量後,B線程讀同一個volatile變量。A線程在寫volatile變量之
前全部可見的共享變量,在B線程讀同一個volatile變量後,將當即變得對B線程可見。

何時須要使用volatile

經過上面的例子,咱們能夠總結下volatile的使用場景。

一般是,存在一個或者多個共享變量,會有線程對他們寫操做,也會有其它線程對他們讀操做。這樣的變量都應該使用volatile修飾。

volatile在標準庫裏的應用

ConcurrentHashMap裏用到了一些volatile的操做,好比:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        ...

能夠看到,用於存儲值的value變量就是volatile類型,這樣能夠保證在多線程讀取的時候,不會讀到過時的值。之因此不會讀到過時的值,是由於根據Java內存模型的happen before原則,對volatile字段的寫入操做先於讀操做,即便兩個線程同時修改和獲取volatile變量,get操做也能拿到最新的值,這是用volatile替換鎖的經典應用場景。

volatile會下降程序執行的效率

不要過分使用volatile,沒必要要的場景沒有必要用volatile修飾變量,儘管這樣作程序也不會出什麼錯。

根據前面的描述,volatile至關於給變量的操做加了「鎖」,每次操做都有加鎖和釋放鎖的動做,效率天然會受影響。

volatile不是萬能的

對volatile常常有一中誤解就是,它能夠保證原子操做。

經過上面的例子,咱們知道,volatile關鍵字能夠保證內存可見性,指令執行的有序性。可是請必定記住,它無法保證原子性。舉個例子你可能比較容易明白。

public class VolatileTest {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();

        for(int i=0;i<10;i++){
            new Thread(() -> {
                for(int j=0;j<1000;j++)
                    test.increase();
            }).start();
        }
        while(Thread.activeCount()>2)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

執行這段代碼,會發現結果每次通常都不一樣,可是確定都小於10*1000。這就是volatile不保證原子性的最好證據。那麼深層次的緣由是什麼呢?

事實上,自增操做包括三個步驟:

  1. 讀取變量的原始值
  2. 進行加1操做
  3. 寫入線程工做內存

既然分了三個步驟,就有可能出現下面這種狀況:

假如某個時刻變量inc的值爲10。

第一步,線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;

第二步, 而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2會直接去主存讀取inc的值,此時inc的值時10;

第三步, 線程2進行加1操做,並把11寫入工做內存,最後寫入主存。

第四步,線程1接着進行加1操做,因爲已經讀取了inc的值,此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。

最後,兩個線程分別進行了一次自增操做後,可是inc只增長了1。

有不少人會在第三步和第四步那裏有疑問,線程2更新inc的值之後,不是會致使線程1工做內存中的值失效嗎?

答案是不會,由於在一個操做中,值只會讀取一次。這個是原子性和可見性區分的核心。

解決方案是使用increase方法使用synchronized同步鎖修飾。具體不展開了。


參考:

相關文章
相關標籤/搜索