當前位置:
首頁 > 知識 > volatile變數與普通變數的區別

volatile變數與普通變數的區別

我們通常會用volatile實現一些需要線程安全的代碼(也有很多人不敢用,因為不了解),但事實上volatile本身並不是線程安全的,相對於synchoronized,它有更多的使用局限性,只能限制在某些特定的場景。本篇文章的目的就是讓大家對 volatile 在本質上有個把握,為了達到這個目的,我們會從java 的內存模型及變數操作的內存管理來說明(不用怕,你會發現很簡單)。


一、內存模型

volatile變數與普通變數的區別

可以將內存簡單分為兩種:工作內存和主內存。所有的數據最終都需要存儲在主內存,工作內存是線程獨有的,線程之間無任何干擾。java的內存模型主要就是定義工作內存和主內存的交互,即工作內存如何從主內存拷貝數據,以入如何寫數據。java 定義了8種原子性操作來完成工作內存與主內存的交互:

  • lock 將對象變成線程獨佔的狀態

  • unlock 將線程獨佔狀態的對象的鎖釋放出來

  • read 從主內存讀數據

  • load 將從主內存讀取的數據寫入工作內存

  • use 工作內存使用對象

  • assign 對工作內存中的對象進行賦值

  • store 將工作內存中的對象傳送到主內存當中

  • write 將對象寫入主內存當中,並覆蓋舊值

這些操作也是有一定的條件限制的:

read 和load,store和write 必須成對出現,即從主內存中讀數據的數據工作內存必須接受;傳遞到主內存的數據,也不可以被拒絕寫入。

assign後的對象必須回寫到緩存

未進行新賦值的對象不允許回寫到主內存

新的變數只能在主內存產生,且未完成初始化的對象不允許在工作內存中使用

對象只允許被一條線程鎖定,且可以被此線程多次鎖定

未被鎖定的對象不允許執行unlock操作

對一個對象執行unlock之前,必須將對象回寫到主內存

java的8種原子性操作,相互之前有一定的約束條件,但並沒有嚴格限制任意兩個操作必須連續出現,只是表示成對出現,這也是為什麼會產生線程不安全性的原因。

介紹了上述的背景知識,那我們就來看一下volatile變數到底和普通變數有啥差別吧


二、volatile變數與普通變數

2.1 volatile 的安全性

下面我們用一個例子來說明volatile變數與普通變數的區別。

假設有兩個線程操作一個主內存的對象,且線程1早於線程2開始(如下例如示一個a++操作))

public class ThreadSafeTest {
public static int a = 0;
public static void increase() {
a++;
}
public static void main (String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() { for (int j = 0; j < 100; j++) {
increase();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() { for (int j = 0; j < 100; j++) {
increase();
}
}
});
t1.start();
t2.start();
}
}

線程2讀取主內存對象(a)時,可能發生在幾個時期:read之前、read之後、load之後、use之後、assign之後、 store之後、write之後(如下圖所示);

volatile變數與普通變數的區別

假設線程1執行了a++,a從0變成了1,還未來得及寫回主內存對象,線程2從主內存對象中讀取的數據a=0;此時線程1寫入主內存a=1,而線程2仍然執行完了a++ ,此時仍然 等於1(應該等於2),實際上,這就上相當於線程2 讀入了一個過期的數據,導致線程不安全。

那如果將a變成volatile對象是否就正確了呢?

volatile對對象的操作做了更嚴格的限制:

  • use之前不進行read和load

  • assign之後必須緊跟store和write

    實際相當於將read load use 三個原子操作變成一個原子操作;將assign-store-write變成一個原子操作。很多文章上都講volatile對所有的線程是可見的,指的就是執行完了assign之後立即就會回寫主內存;在任意一個線程讀取主內存對象時,都會刷新主內存。在主內存中表現是數據一致性的,但是各線程內存當中卻不一定是一致性的。

    同樣是上面的代碼,換成volatile

  • public class ThreadSafeTest {
    public static volatile int a = 0;
    public static void increase() {
    a++;
    }
    public static void main (String[] args) {
    Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
    for (int j = 0; j < 100; j++) {
    increase();
    }
    }
    });
    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    for (int j = 0; j < 100; j++) {
    increase();
    }
    }
    });
    t1.start();
    t2.start();
    }
    }

運行後發現,也拿不到正確的結果(如果拿到請把j的數值調大)。操你媽,不是說是線程安全的變數嗎?為啥也不正確?

這是因為線程內部的數據仍然有可能存在不一致性,比如,如果線程2讀取數據時,處在線程1use之後,但線程1此時還未來得及回寫主緩存,這時候線程2使用到的數據仍然是0,兩個線程同時對0++,得到的結果只會是1,而不是理想中的2。

volatile變數與普通變數的區別

2.2 volatile 的線程安全是有條件的

即然volatile 是非線程安全的,那要它還有什麼用呢?如果你看過我寫過的「線程安全」的文章應該知道,所有的對象都是相對線程安全的,也就是有條件的。volatile的線程安全當然也是有條件的,它是對synchronized這一重量級線程同步的一種補充,其整體性能上優於synchronized。那volatile的線程安全的條件是什麼呢?適合使用在哪些場景?

《java虛擬機》給出兩個條件:

  • 運算結果並不依賴變數的當前值(即結果對產生中間結果不依賴),或者能夠確保只有單一的線程修改變數的值

  • 變數不需要與其它的狀態變數共同參與不變約束(我認為此條多此一舉,這個其它變數也必須得是線程安全的才行)

那適合哪些場景呢?這個我就不一一舉例了

volatile變數與普通變數的區別

原文作者

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Java團長 的精彩文章:

Spring Security 使用總結
輕鬆學JVM(一)——基本原理
面試分享:一年經驗初探阿里巴巴前端社招
程序員的小目標:升職加薪
你知道么?static關鍵字有5種用法

TAG:Java團長 |