寫在開篇之前
記得我有次面試的時候,問到了多線程這塊,面試官問了我對于volatile關鍵字的理解,當時我是說在多線程并發編程中,對于共享變量,用volatile關鍵字可以保證每個線程讀取變量值的時候都是最新的。但當他繼續問我為什么volatile這個關鍵字可以保證讀取的值是最新的時候,我發現原來我理解的可能還是不夠深入。于是回來之后,我便查閱資料,深入研究了一下volatile關鍵字的作用。下面就分享一下我對volatile關鍵字的理解。
CPU高速緩存
大家都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由于程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由于CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU里面就有了高速緩存。也就是,當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。
并發編程中通常會遇到的三個問題
原子性問題,可見性問題,有序性問題。我們先具體看一下這三個概念:
1.原子性
原子性:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
2.可見性:
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
3.有序性:
即程序執行的順序按照代碼的先后順序執行。一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
對于原子性,volatile關鍵字只能保證每次讀取的是最新的值,但是沒辦法保證對變量的操作的原子性。Java只保證了對基本數據類型的讀取和賦值是原子性操作,如果要實現更大范圍操作的原子性,可以通過synchronized和Lock來實現。由于synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。
對于可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
對于有序性,在Java里面,可以通過volatile關鍵字來保證一定的“有序性”。另外還可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼,自然就保證了有序性。
我們回到主題,前面說volatile關鍵字可以保證可見性和有序性,下面介紹具體是如何保證的。
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
1、使用volatile關鍵字會強制將修改的值立即寫入主存,當線程2進行修改時,會導致線程1的工作內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效),由于線程1的工作內存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
2、volatile關鍵字能禁止進行指令重排序,所以volatile能在一定程度上保證有序性。
Volatile的使用場景:
需要保證操作是原子性操作,才能保證使用volatile關鍵字的程序在并發時能夠正確執行。比如:狀態標記量,雙重校驗
在Java中雙重檢查模式無效的原因是在不同步的情況下引用類型不是線程安全的。對于除了long和double的基本類型,雙重檢查模式是適用 的,如果將引用類型聲明為volatile,雙重檢查模式就可以工作了。