volatile關鍵字的作用及使用場景

volatile關鍵字是java虛擬機提供的最輕量級的同步機制。在併發的場景下,我們都習慣於使用synchronized鎖進行同步,其實,有些簡單的場景下,我們也可以使用volatile來代替,但volatile有它本身的使用規則,不能濫用,要結合它自身的特性把它應用於適合的場景下。下面就來簡單敘述一下volatile關鍵字的作用,及其適用場景。

一、volatile的特性

被volatile修飾的變量將會有兩個特性,第一是保證此變量對所有的線程的可見性;第二是禁止指令重排。

1、可見性

這裏的可見性,指的是一條線程修改了被volatile修飾的變量的值後,新值對於其他線程也是立即得知的。
如下圖,當某時刻線程T1對變量V作了B操作,那麼線程T2、T3也可以馬上感知到V被作了B操作。
在這裏插入圖片描述
熟悉jvm內存模型的童鞋,應該知道變量(不包括局部變量和方法變量)都是存在於主內存中,而每個線程對此變量的所有操作都必須通過工作內存,工作內存會不斷的從主內存讀取數據,或往主內存寫入數據。在多線程對同一個變量進行操作時,會存在“可見性”問題。而volatile關鍵字來修飾變量,就是使得線程每次使用變量時都會強制先從主內存刷新到工作內存,每次修改變量時都會強制立刻把工作內存中發生的改變回寫到主內存中。這樣就實現了“當一個線程對變量操作時,其他線程都可以感知到”這樣的說法。
那麼,volatile就一定是線程安全了嗎?然而並不是,volatile只是增強了可見性,但都是有前提的。對一個變量的操作,其實很多都不是原子操作,比如一個整型全局變量a,a++這樣的操作就不是原子操作,它可以拆解成3條指令:

1、從主內存取出變量a的值,load到工作內存中;
2、工作內存中的a值加1;
3、工作內存中新的a值回寫到主內存。

由此可見,就算volatile保證了變量的可見性,但在多線程的環境下,依然保證不了線程安全,因爲多線程的讀取、寫入總會存在差異,此時應該使用原子類乃至鎖機制方可解決。(以上的例子不是很嚴謹,因爲一條高級語言指令也是由很多條機器指令完成)

2、禁止指令重排

CPU和編譯器爲了提升程序執行的效率,會按照一定的規則允許進行指令重排。指令重排在單線程是沒什麼問題的,但如果是多線程的情況下,就有可能產生意想不到的現象。如下程序:

class UnsafeExam {
  int x = 0;
  boolean v = false;
  //volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 這裏 x 會是多少呢?
      System.out.print(x);
    }
  }
}

由以上程序,如果編譯器優化的時候,把x = 42和v = true換了個位置,在多線程的環境下,後果是不堪設想的,x的值輸出可能是0或42。如果變量v加個volatile修飾,相當於加了個內存屏障,指令重排時不能把後面的指令重排序到內存屏障之前的位置。

二、性能

volatile變量讀操作的性能消耗與普通變量差別不大,單是寫操作可能會慢一些,因爲他需要在本地代碼插入許多內存屏障指令來保證處理器不發生亂序執行。不過即便如此,大多數場景volatile關鍵字的總開銷要比鎖來的更低。我們在volatile與鎖中選擇的唯一判斷一句僅僅是volatile語義能否滿足使用場景的需求。

三、適用場景

由於volatile只能保證可見性,則使用時必須遵守以下原則,否則需要考慮使用原子類乃至鎖機制。

  1. 運算結果不依賴變量的當前值,或者能確保只有單一的線程修改變量的值;
  2. 變量不需要與其他狀態變量共同參與不變約束。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章