volatile關鍵字作用與內存可見性、指令重排序概述[JAVA]

在理解volotile關鍵字的作用之前,先粗略解釋下內存可見性與指令重排序。

1. 內存可見性

Java內存模型規定,對於多個線程共享的變量,存儲在主內存當中,每個線程都有自己獨立的工作內存,並且線程只能訪問自己的工作內存,不可以訪問其它線程的工作內存。工作內存中保存了主內存中共享變量的副本,線程要操作這些共享變量,只能通過操作工作內存中的副本來實現,操作完畢之後再同步回到主內存當中,其JVM內存模型大致如下圖。

這裏寫圖片描述

而JAVA內存模型規定工作內存與主內存之間的交互協議,其中包括8種原子操作:

1) lock:將主內存中的變量鎖定,爲一個線程所獨佔 
2) unclock:將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量 
3) read:將主內存中的變量值讀到工作內存當中 
4) load:將read讀取的值保存到工作內存中的變量副本中。 
5) use:將值傳遞給線程的代碼執行引擎 
6) assign:將執行引擎處理返回的值重新賦值給變量副本 
7) store:將變量副本的值存儲到主內存中。 
8) write:將store存儲的值寫入到主內存的共享變量當中。

其中lock和unlock定義了一個線程訪問一次共享內存的界限,而其它操作下線程的工作內存與主內存的交互大致如下圖所示。

這裏寫圖片描述

從上圖可以看出,read and load 主要是將主內存中數據複製到工作內存中,use and assign則主要是使用數據,並將改變後的值寫入到工作內存,store and write則是用工作內存數據刷新主存相關內容。

但是以上的一系列操作並不是原子的,也既是說在read and load之後,主內存中變量的值發生了改變,這時再use and assign則並不是取的最新的值,而我認爲的內存可見性可粗略描述爲,如果數據A在一個線程中的改變能夠立即被其他線程可見,那麼則說數據A具有內存可見性 ,也既是說如果數據A具有內存可見性,那麼即使一個線程在read and load之後,數據A的值被改變了,在use and assign時也能獲取到數據A最新的值並使用,那麼該如何保證線程在每次use and assign時都是獲取的數據A的最新的值呢?

其實只要線程在每次use and assign時都是直接從主內存中獲取數據A的值,就能夠保證每次use and assign都是獲取的數據A的最新的值,也既是能保證數據A的內存可見性,而volatile關鍵字的作用之一便是系統每次用到被它修飾過的變量時都是直接從主內存當中提取,而不是從Cache中提取,同時對於該變量的更改會馬上刷新回主存,以使得各個線程取出的值相同,這裏的Cache可以理解爲線程的工作內存。當然了volatile關鍵字還有另外一個非常重要的作用,即局部阻止指令重排序。

(注:synchronized或其它加鎖,也能保證內存可見性,但實現方式略有不同,也不在本文的討論範圍內)

2. 指令重排序

首先看下以下線程A和線程B的部分代碼:

線程A:
content = initContent();    //(1)
isInit = true;              //(2)
  • 1
  • 2
  • 3
線程B
while (isInit) {            //(3)
    content.operation();    //(4)
}
  • 1
  • 2
  • 3
  • 4

從常規的理解來看,上面的代碼是不會出問題的,但是JVM可以對它們在不改變數據依賴關係的情況下進行任意排序以提高程序性能(遵循as-if-serial語義,即不管怎麼重排序,單線程程序的執行結果不能被改變),而這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不會被編譯器和處理器考慮,也即是說對於線程A,代碼(1)和代碼(2)是不存在數據依賴性的,儘管代碼(3)依賴於代碼(2)的結果,但是由於代碼(2)和代碼(3)處於不同的線程之間,所以JVM可以不考慮線程B而對線程A中的代碼(1)和代碼(2)進行重排序,那麼假設線程A中被重排序爲如下順序:

線程A:
isInit = true;              //(2)
content = initContent();    //(1)
  • 1
  • 2
  • 3

對於線程B,則可能在執行代碼(4)時,content並沒有被初始化,而造成程序錯誤。那麼應該如何保證絕對的代碼(2) happens-before 代碼(3)呢?沒錯,仍然可以使用volatile關鍵字。

volatile關鍵字除了之前提到的保證變量的內存可見性之外,另外一個重要的作用便是局部阻止重排序的發生,即保證被volatile關鍵字修飾的變量編譯後的順序與 也即是說如果對isInit使用了volatile關鍵字修飾,那麼在線程A中,就能保證絕對的代碼(1) happens-before 代碼(2),也便不會出現因爲重排序而可能造成的異常。

3. 總結

綜上所訴,volatile關鍵字最主要的作用是: 
1) 保證變量的內存可見性 
2) 局部阻止重排序的發生

4. 附錄 - happens-before原則

英文原文:

  • Each action in a thread happens before everyaction in that thread that comes later in the program’s order.
  • An unlock on a monitor happens before everysubsequent lock on that same monitor.
  • A write to a volatile field happens before everysubsequent read of that same volatile.
  • A call to start() on a thread happens before anyactions in the started thread.
  • All actions in a thread happen before any otherthread successfully returns from a join() on that thread.

中文描述:

  • 程序順序規則:一個線程中的每個操作,happens-before 於該線程中的任意後續操作。 (並沒有)
  • 監視器鎖規則:對一個監視器鎖的解鎖,happens-before 於隨後對這個監視器鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens-before 於任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
  • Thread.start()的調用會happens-before於啓動線程裏面的動作。
  • Thread中的所有動作都happens-before於其他線程從Thread.join中成功返回。

其中第一條程序順序規則並不合理,因爲在線程中只有存在數據依賴性纔不會被重排序,而沒有任何數據依賴性的操作,依然可能被編譯器重排序。

5. 參考文獻

[1] Brian Goetz.Java併發編程實戰.機械工業出版社.2012 
[2] http://ifeve.com/easy-happens-before/ 
[3] http://www.infoq.com/cn/articles/java-memory-model-2/ 
[4] http://www.cnblogs.com/mengyan/archive/2012/08/22/2651575.html 
[5] http://my.oschina.net/chihz/blog/58035 
[6] http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 
[7] http://ifeve.com/jvm-reordering/ 

[8] ……

轉載於https://blog.csdn.net/t894690230/article/details/50588129

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章