天天看點

Java并發程式設計-volatile可見性的介紹

前言

要學習好Java的多線程,就一定得對volatile關鍵字的作用機制了熟于胸。最近部落客看了大量關于volatile的相關部落格,對其有了一點初步的了解和認識,下面通過自己的話叙述整理一遍。

有什麼用?

volatile主要對所修飾的變量提供兩個功能

  • 可見性
  • 防止指令重排序

本篇部落客要對volatile可見性進行探讨,以後發表關于指令重排序的博文。

什麼是可見性?

一圖勝千言

Java并發程式設計-volatile可見性的介紹

上圖已經把JAVA記憶體模型(JMM)展示得很詳細了,簡單概括一下

1. 每個Thread有一個屬于自己的工作記憶體(可以了解為每個廚師有一個屬于自己的鐵鍋)

2. 所有Thread共用一個主記憶體(餐廳所有的廚師共用同一個冰箱)

3. 每個Thread操作資料之前都會去主記憶體中擷取資料(廚師炒菜之前都要去冰箱裡拿食材)

  • Thread:廚師
  • 工作記憶體:鐵鍋
  • store&load:放熟食,取食材
  • 主記憶體:冰箱

讀者可思考以下情景:

餐廳來了一位顧客點了一份紅燒肉,此時有兩位大廚(假設大廚之間互不通信),由于互不通信,是以兩位大廚都打開冰箱取出食材開始炒菜。

最後炒出了兩份紅燒肉,顧客隻要一份。為什麼會造成這種結果?

由于大廚之間沒有可見性。

将此情景放在JAVA中即是:

線程A從主記憶體中取了一個變量到工作記憶體中,操作完畢後沒有及時放回主記憶體中,于是線程B去取這個變量已經過期了,取的是線程A操作之前的變量。

如何擁有可見性?

先介紹一下Java記憶體模型中定義的8種工作記憶體與主記憶體之間的原子操作

  • lock( 鎖定 ):作用于主記憶體的變量,把一個變量辨別為一條線程獨占的狀态。
  • unlock(解鎖):作用于主記憶體的變量,把一個處于鎖定的變量釋放出來,釋放變量才可以被其他線程鎖定。
  • read(讀取):作用于主記憶體的變量,把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用。
  • load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中。
  • use(使用):作用于工作記憶體種的變量,它把工作記憶體中一個變量的值傳遞給執行引擎,每當虛拟機遇到一個需要使用到變量的值的位元組碼指令時将會執行這個操作。
  • assign(指派):作用于工作記憶體中的變量,它把一個從執行引擎接收到的值賦給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。
  • store(存儲):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳送到主記憶體中,以便随後的write操作使用
  • write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的值放入主記憶體的變量中。

讀取指派一個普通變量的情況

Java并發程式設計-volatile可見性的介紹

當線程1對主記憶體對象發起read操作到write操作套流程的時間裡,線程2随時都有可能對這個主記憶體對象發起第二套操作

- 有什麼危害呢?

假設主記憶體中有一個

線程1和線程2分别執行一次,理想狀态下最終a的值為2.

線程1在執行了assign操作之後變量a的真實值已經從0變成了1,但是這個過程發生在工作記憶體中對其他線程不可見,若線程2此時對變量a的操作,讀取到的值仍然為0,因為沒有可見性,線程2的操作也僅僅是重複了線程1的操作,再次讓a從0變成了1。并沒有達到期望的a=2。

讀取指派一個volatile變量的情況

Java并發程式設計-volatile可見性的介紹

volatile變量對對象的操作更嚴格:

- use之前不能被read&load

- assign之後必須緊跟store&write

也就是說 read-load-use 和 assign-store-write成為了兩個不可分割的原子操作

盡管這時候在use和assign之間依然有一段真空期,有可能變量會被其他線程讀取,但是無論在哪一個時間點主記憶體的變量和任一工作記憶體的變量的值都是相等的。這個特性就導緻了volatile變量不适合參與到依賴目前值的運算,如自增。

那麼依靠可見性的特點volatile可以用在哪些地方呢?

《Java虛拟機》提到:

運算結果并不依賴變量的目前值(即結果對産生中間結果不依賴),或者能夠確定隻有單一的線程修改變量的值

通常volatile用做儲存某個狀态的boolean值。

部分參考自

  • volatile變量與普通變量的差別
  • <<深入了解Java虛拟機 進階特性與最佳實踐>>