1. 程式人生 > >Java 關鍵字之volitle

Java 關鍵字之volitle

面試總考volitle關鍵字的作用,今天我特意花時間深究了一下volitle。以下是本人經驗總結。

前言

在瞭解volatile變數作用前,先需要明白一些概念:

什麼是原子操作?
所謂原子操作,就是"不可中斷的一個或一系列操作" , 在確認一個操作是原子的情況下,多執行緒環境裡面,我們可以避免僅僅為保護這個操作在外圍加上效能昂貴的鎖,甚至藉助於原子操作,我們可以實現互斥鎖。

關於java中的原子性?
原子性可以應用於除long和double之外的所有基本型別之上的“簡單操作”。對於讀取和寫入出long double之外的基本型別變數這樣的操作,可以保證它們會被當作不可分(原子)的操作來操作。 因為JVM的版本和其它的問題,其它的很多操作就不好說了,比如說++操作在C++中是原子操作,但在Java中就不好說了。 另外,Java提供了AtomicInteger等原子類。再就是用原子性來控制併發比較麻煩,也容易出問題。

volatile原理是什麼?
Java中volatile關鍵字原義是“不穩定、變化”的意思
使用volatile和不使用volatile的區別在於JVM記憶體主存和執行緒工作記憶體的同步之上。volatile保證變數線上程工作記憶體和主存之間一致。
其實是告訴處理器, 不要將我放入工作記憶體, 請直接在主存操作我.

Volatile實現原理詳解

那麼Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操作CPU會做什麼事情。

Java程式碼: instance = new Singleton();//instance是volatile變數
彙編程式碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock

addl $0x0,(%esp);
有volatile變數修飾的共享變數進行寫操作的時候會多第二行彙編程式碼,通過查IA-32架構軟體開發者手冊可知。

lock字首的指令在多核處理器下會引發了兩件事情。
1.將當前處理器快取行的資料會寫回到系統記憶體。
2.這個寫回記憶體的操作會引起在其他CPU裡快取了該記憶體地址的資料無效。

處理器為了提高處理速度,不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完之後不知道何時會寫到記憶體,如果對聲明瞭Volatile變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作的時候,會強制重新從系統記憶體裡把資料讀到處理器快取裡。

一個處理器的快取回寫到記憶體會導致其他處理器的快取無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部快取和其他處理器快取的一致性。在多核處理器系統中進行操作的時候,IA-32 和Intel 64處理器能嗅探其他處理器訪問系統記憶體和它們的內部快取。它們使用嗅探技術保證它的內部快取,系統記憶體和其他處理器的快取的資料在總線上保持一致。例如在Pentium和P6 family處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫記憶體地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的快取行,在下次訪問相同記憶體地址時,強制執行快取行填充。

接下來是測試 :(通過測試能更好的發現和分析問題)
申明瞭幾種整形的變數,開啟100個執行緒同時對這些變數進行++操作,發現結果差異很大:

Execute End:
Atomic: 100000
VInteger: 38790
Integer: 68749
Source i: 99205
Source Vi: 99286
也就是說除了Atomic,其他的都是錯誤的。

我們通過一些疑問,來解釋一下。

1:為什麼會產生錯誤的資料?
多執行緒引起的,因為對於多執行緒同時操作一個整型變數在大併發操作的情況下無法做到同步,而Atom提供了很多針對此類執行緒安全問題的解決方案,因此解決了同時讀寫操作的問題。

2:為什麼會造成同步問題?
Java多執行緒在對變數進行操作的時候,實際上是每個執行緒會單獨分配一個針對i值的拷貝(獨立記憶體區域),但是申明的i值確是在主記憶體區域中,當對i值修改完畢後,執行緒會將自己記憶體區域塊中的i值拷貝到主記憶體區域中,因此有可能每個執行緒拿到的i值是不一樣的,從而出現了同步問題。

3:為什麼使用volatile修飾integer變數後,還是不行?
因為volatile僅僅只是解決了儲存的問題,即i值只是保留在了一個記憶體區域中,但是i++這個操作,涉及到獲取i值、修改i值、儲存i值(i=i+1),這裡的volatile只是解決了儲存i值得問題,至於獲取和修改i值,確是沒有做到同步。

4:既然不能做到同步,那為什麼還要用volatile這種修飾符?
主要的一個原因是方便,因為只需新增一個修飾符即可,而無需做物件加鎖、解鎖這麼麻煩的操作。但是本人不推薦使用這種機制,因為比較容易出問題(髒資料),而且也保證不了同步。

5:那到底如何解決這樣的問題?
第一種:採用同步synchronized解決,這樣雖然解決了問題,但是也降低了系統的效能。
第二種:採用原子性資料Atomic變數,這是從JDK1.5開始才存在的針對原子性的解決方案,這種方案也是目前比較好的解決方案了。

6:Atomic的實現基本原理?
首先Atomic中的變數是申明為了volatile變數的,這樣就保證的變數的儲存和讀取是一致的,都是來自同一個記憶體塊,然後Atomic提供了getAndIncrement方法,該方法對變數的++操作進行了封裝,並提供了compareAndSet方法,來完成對單個變數的加鎖和解鎖操作,方法中用到了一個UnSafe的物件,現在還不知道這個UnSafe的工作原理(似乎沒有公開原始碼)。Atomic雖然解決了同步的問題,但是效能上面還是會有所損失,不過影響不大,網上有針對這方面的測試,大概50million的操作對比是250ms : 850ms,對於大部分的高效能應用,應該還是夠的了。

總結

Java語言規範中指出:為了獲得最佳速度,允許執行緒儲存共享成員變數的私有拷貝,而且只當執行緒進入或者離開同步程式碼塊時才與共享成員變數的原始值對比。

這樣當多個執行緒同時與某個物件互動時,就必須要注意到要讓執行緒及時的得到共享成員變數的變化。

而volatile關鍵字就是提示VM:對於這個成員變數不能儲存它的私有拷貝,而應直接與共享成員變數互動。

使用建議:在兩個或者更多的執行緒訪問的成員變數上使用volatile。當要訪問的變數已在synchronized程式碼塊中,或者為常量時,不必使用。

由於使用volatile遮蔽掉了VM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。