眾所周知,volatile關鍵字可以讓線程的修改立刻通知其他的線程,從而達到數據一致的作用。那么它具體涉及到哪些內容呢?
關於緩存
計算機最大的存儲空間就是磁盤(硬盤),但是訪問的速度也是最慢的,價格最便宜;再就是內存,容量更小,造價更高,但是速度也更快。不過跟cpu的計算速度比起來,那就太慢了。可以想像,如果cpu每次計算都要從內存讀取數據,那大部分的時間估計都浪費在這上面了。所以就引入了緩存的概:
緩存的結構大概時這樣的,從1級到3級速度越來越慢,最后通過總線與內存連接。如果時多核多cpu,那么結構大概是這樣的:
多線程造成的緩存不一致
由於現在大部分的機器都有多個cpu,這就導致如果時運行多線程的任務,就可能運行在不同的cpu上。試想一下:
int a = 0;
int b = a;
b += 1;
如果開啟兩個線程執行,我們想要的結果是3,但是最后的結果只是2。這是因為在做加法運算的時候,cpu會先把a的值讀入cpu的緩存,然后更新緩存,在更新內存。很有可能兩個線程分散在兩個cpu,每個都是對自己緩存內的數據進行讀寫,這樣就造成了結果不一致的現象。
volatile的作用
volatile的作用就是當一個線程更新某個volatile聲明的變量時,會通知其他的cpu使緩存失效,從而其他cpu想要做更新操作時,需要從內存重新讀取數據。具體的通知方式,一種是通過某種協議,比如MESI;再就是對總線加鎖,控制變量的讀取。具體硬件上怎么個流程,我就搞不清楚了...
並發
這里還需要強調的時,並發編程涉及的三個特性:原子性、可見性、有序性。就好像分布式里面的cap一樣,需要熟知。先來通俗的描述下:
原子性
即要么全做,要么全部做。比如從a銀行轉錢到b銀行。
在編程中,除了long或者double外的變量更新就是原子操作。long和double除外,是因為它們在32位的操作系統上,會被分成兩部分進行更新,此時就不是原子的。
再比如最常見的i++也不是原子的,它相當於先讀取i
,進行+1操作
,更新
三個步驟進行。
可見性
多個線程訪問同一個變量時,這個變量被修改后,能被其他的線程看到。
有序性
比如
int a = 10;
int r = 2;
a = a + 3;
r = a*a;
這段代碼有可能進行指令的重排,從而導致結果跟預期的不一致。指令的重排需要按照happens-before
原則,比如:
- 程序次序原則,一個線程內,按照書寫的順序執行
- 鎖定原則,lock前后執行
- volatile原則,volatile變量前后執行
- 傳遞原則,如果a需要調用b,那么a就會在b的前面
...
等等...
volatile的特性
volatile只能保證變量的可見性、有序性,但是不能保證原子性。因此可以用它來做double-check,但是不能來做i++的操作。如果想要實現i++的可靠性,必須依賴於synchronized、lock或者atomicXXX來實現。
參考
- 海子的《Java並發編程:volatile關鍵字解析》:http://www.cnblogs.com/dolphin0520/p/3920373.html
- liuxiaopeng的《Java 並發編程:volatile的使用及其原理》:https://www.cnblogs.com/paddix/p/5428507.html
- double check http://blog.csdn.net/dl88250/article/details/5439024
- cpu緩存知識:http://blog.jobbole.com/36263/