物理角度:
由於計算機的儲存裝置和cpu的運算速度有幾個數量級的差距,所以現代計算機系統加入一層速度接近
cpu的快取記憶體(
cache),
但cache帶來乙個問題:
快取一致性問題:
在多處理器系統中,每個處理器機油自己的cache(工作記憶體),又共享同一主記憶體。
舉例:當程式在執行過程中,會將運算需要的資料從主存複製乙份到cpu的快取記憶體當中,那麼
cpu進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。舉個簡單的例子,比如下面的這段**:
i = i + 1;
當執行緒執行這個語句時,會先從主存當中讀取i的值,然後複製乙份到快取記憶體當中,然後
cpu
執行指令對
i進行加
1操作,然後將資料寫入快取記憶體,最後將快取記憶體中
i最新的值重新整理到主存當中。
這個**在單執行緒中執行是沒有任何問題的,但是在多執行緒中執行就會有問題了。在多核 cpu 中,每條執行緒可能執行於不同的
cpu
中,因此
每個執行緒執行時有自己的快取記憶體(對單核
cpu來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。比如同時有兩個執行緒執行這段**,假如初始時
i的值為
0,那麼我們希望兩個執行緒執行完之後
i的值變為
2。但是事實會是這樣嗎?
可能出現這種情況:初始時,兩個執行緒分別讀取i的值存入各自所在的
cpu
的快取記憶體當中,然後 執行緒
1 進行加
1操作,然後把
i的最新值
1寫入到記憶體。此時執行緒
2的快取記憶體當中
i的值還是
0,進行加
1操作之後,
i的值為
1,然後執行緒2把
i的值寫入記憶體。最終結果
i的值是
1,而不是
2。這就是著名的快取一致性問題。通常稱這種被多個執行緒訪問的變數為共享變數。
要使 volatile 變數提供理想的執行緒安全,必須同時滿足下面兩個條件:
volatile的關鍵字的語義
:1保證修飾的變數對所有執行緒的可見性:
某一線程修改了volatile修飾的變數的值,新值對於其他執行緒來說是可以立即得知到。
例子:
先看一段**,假如執行緒1先執行,執行緒2後執行:
//執行緒1
boolean
stop
=false
;while(!
stop)
//執行緒2
stop
=true
;這段**是很典型的一段**,很多人在中斷執行緒時可能都會採用這種標記辦法。但是事實上,這段**會完全執行正確麼?即一定會將執行緒中斷麼?不一定,也許在大多數時候,這個**能夠把執行緒中斷,但是也有可能會導致無法中斷執行緒(雖然這個可能性很小,但是只要一旦發生這種情況就會造成死迴圈了)。
下面解釋一下這段**為何有可能導致無法中斷執行緒。在前面已經解釋過,每個執行緒在執行過程中都有自己的工作記憶體,那麼
執行緒1在執行的時候,會將
stop
變數的值拷貝乙份放在自己的工作記憶體當中。
那麼當執行緒2
更改了stop
變數的值之後,但是還沒來得及寫入主存當中,
執行緒2轉去做其他事情了,那麼
執行緒1由於不知道
執行緒2對
stop
變數的更改,因此還會一直迴圈下去。但是用
volatile
修飾之後就變得不一樣了:
- 使用
volatile
關鍵字會強制將修改的值立即寫入主存;
· 使用
volatile
關鍵字的話,當
執行緒2進行修改時,會導致
執行緒1的工作記憶體中快取變數
stop
的快取行無效(
反映到硬體層的話,就是cpu的l1或者l2快取中對應的快取行無效
);·
由於執行緒1
的工作記憶體中快取變數
stop
的快取行無效,所以
執行緒1再次讀取變數
stop
的值時會去主存讀取。
· 那麼在
執行緒2修改
stop
值時(當然這裡包括2個操作,修改執行緒2工作記憶體中的值,然後將修改後的值寫入記憶體),會使得
執行緒1的工作記憶體中快取變數
stop
的快取行無效,然後
執行緒1讀取時,發現自己的快取行無效,它會等待快取行對應的主存位址被更新之後,然後去對應的主存讀取最新的值。
那麼執行緒1讀取到的就是最新的正確的值。
2禁止指令重排序:
保證變數的賦值操作的順序和程式**中的執行順序一致。
例子:·
當程式執行到
volatile
變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見,在其後面的操作肯定還沒有進行;
·在進行指令優化時,不能將在對
volatile
變數訪問的語句放在其後面執行,也不能把
volatile
變數後面的語句放到其前面執行。
可能上面說的比較繞,舉個簡單的例子:
//x、y為非volatile變數//flag為volatile變數x =
2;
//語句1y =
0;
//語句2
flag
=true
; //語句3x =
4;
//語句4y =
-1;
//語句5
由於flag變數為
volatile
變數,那麼在進行指令重排序的過程的時候,不會將
語句3放到
語句1、
語句2前面,也不會講
語句3放到
語句4、
語句5後面。但是要注意
語句1和
語句2的順序、
語句4和
語句5的順序是不作任何保證的。
並且volatile
關鍵字能保證,執行到
語句3時
,語句1
和語句2
必定是執行完畢了的,且
語句1和
語句2的執行結果對
語句3、
語句4、
語句5是可見的。
觀察加入volatile關鍵字和沒有加入
volatile
關鍵字時所生成的彙編**發現,加入
volatile
關鍵字時,會多出乙個
lock
字首指令
lock字首指令實際上相當於乙個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:· 它
確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
· 它會
強制將對快取的修改操作立即寫入主存;
·如果是寫操作,它會導致其他cpu中對應的快取行無效。
注意:v
olatile不能保證變數的原子性
理解volatile關鍵字
1.可見性 2.禁止指令重排序class factory public static factory getinstance return factory 2.1 dcl疑問解釋 synchronized 快中的非空判斷 這裡做判斷是防止在初始化factory的時候,有兩個執行緒進入,其中乙個執行緒...
volatile關鍵字理解
public class test read start new thread update start 執行結果 執行緒update會一直執行,執行緒read不會執行,執 況一直如此。原因分析 執行緒read,無法感知init value的變化,因為執行緒read中讀到的init value值一直...
如何理解volatile關鍵字
暫時由於查了好多網路上的資料都沒有具體的說明,有範圍也很大沒有看懂,後面如果理解有錯再修改。1.全域性共享變數非volatile 我是這樣理解的,對於多執行緒中,多個執行緒啟動時,部分先啟動的執行緒會把全域性變數拷貝乙個副本到自己的執行緒棧,有的則是啟動 還沒有read load變數到自己的本地棧空...