java:彻底搞懂volatile关键字
對(duì)于volatile這個(gè)關(guān)鍵字,相信很多朋友都聽說過,甚至使用過,這個(gè)關(guān)鍵字雖然字面上理解起來比較簡(jiǎn)單,但是要用好起來卻不是一件容易的事。
這篇文章將從多個(gè)方面來講解volatile,讓你對(duì)它更加理解。
計(jì)算機(jī)中為什么會(huì)出現(xiàn)線程不安全的問題
volatile既然是與線程安全有關(guān)的問題,那我們先來了解一下計(jì)算機(jī)在處理數(shù)據(jù)的過程中為什么會(huì)出現(xiàn)線程不安全的問題。
大家都知道,計(jì)算機(jī)在執(zhí)行程序時(shí),每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中會(huì)涉及到數(shù)據(jù)的讀取和寫入。由于程序運(yùn)行過程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的,這時(shí)就存在一個(gè)問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度比起來要慢的多,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進(jìn)行,會(huì)大大降低指令執(zhí)行的速度。
為了處理這個(gè)問題,在CPU里面就有了高速緩存(Cache)的概念。當(dāng)程序在運(yùn)行過程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。
我舉個(gè)簡(jiǎn)單的例子,比如cpu在執(zhí)行下面這段代碼的時(shí)候,
會(huì)先從高速緩存中查看是否有t的值,如果有,則直接拿來使用,如果沒有,則會(huì)從主存中讀取,讀取之后會(huì)復(fù)制一份存放在高速緩存中方便下次使用。之后cup進(jìn)行對(duì)t加1操作,然后把數(shù)據(jù)寫入高速緩存,最后會(huì)把高速緩存中的數(shù)據(jù)刷新到主存中。
這一過程在單線程運(yùn)行是沒有問題的,但是在多線程中運(yùn)行就會(huì)有問題了。在多核CPU中,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核CPU來說,其實(shí)也會(huì)出現(xiàn)這種問題,只不過是以線程調(diào)度的形式來分別執(zhí)行的,本次講解以多核cup為主)。這時(shí)就會(huì)出現(xiàn)同一個(gè)變量在兩個(gè)高速緩存中的值不一致問題了。
例如:
兩個(gè)線程分別讀取了t的值,假設(shè)此時(shí)t的值為0,并且把t的值存到了各自的高速緩存中,然后線程1對(duì)t進(jìn)行了加1操作,此時(shí)t的值為1,并且把t的值寫回到主存中。但是線程2中高速緩存的值還是0,進(jìn)行加1操作之后,t的值還是為1,然后再把t的值寫回主存。
此時(shí),就出現(xiàn)了線程不安全問題了。
Java中的線程安全問題
上面那種線程安全問題,可能對(duì)于不同的操作系統(tǒng)會(huì)有不同的處理機(jī)制,例如Windows操作系統(tǒng)和Linux的操作系統(tǒng)的處理方法可能會(huì)不同。
我們都知道,Java是一種夸平臺(tái)的語言,因此Java這種語言在處理線程安全問題的時(shí)候,會(huì)有自己的處理機(jī)制,例如volatile關(guān)鍵字,synchronized關(guān)鍵字,并且這種機(jī)制適用于各種平臺(tái)。
Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類似于前面說的物理內(nèi)存),每個(gè)線程都有自己的工作內(nèi)存(類似于前面的高速緩存)。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主存進(jìn)行操作。并且每個(gè)線程不能訪問其他線程的工作內(nèi)存。
由于java中的每個(gè)線程有自己的工作空間,這種工作空間相當(dāng)于上面所說的高速緩存,因此多個(gè)線程在處理一個(gè)共享變量的時(shí)候,就會(huì)出現(xiàn)線程安全問題。
這里簡(jiǎn)單解釋下共享變量,上面我們所說的t就是一個(gè)共享變量,也就是說,能夠被多個(gè)線程訪問到的變量,我們稱之為共享變量。在java中共享變量包括實(shí)例變量,靜態(tài)變量,數(shù)組元素。他們都被存放在堆內(nèi)存中。
volatile關(guān)鍵字
上面扯了一大堆,都沒提到volatile關(guān)鍵字的作用,下面開始講解volatile關(guān)鍵字是如何保證線程安全問題的。
可見性
什么是可見性?
意思就是說,在多線程環(huán)境下,某個(gè)共享變量如果被其中一個(gè)線程給修改了,其他線程能夠立即知道這個(gè)共享變量已經(jīng)被修改了,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候,最終會(huì)去內(nèi)存中讀取,而不是從自己的工作空間中讀取。
例如我們上面說的,當(dāng)線程1對(duì)t進(jìn)行了加1操作并把數(shù)據(jù)寫回到主存之后,線程2就會(huì)知道它自己工作空間內(nèi)的t已經(jīng)被修改了,當(dāng)它要執(zhí)行加1操作之后,就會(huì)去主存中讀取。這樣,兩邊的數(shù)據(jù)就能一致了。
假如一個(gè)變量被聲明為volatile,那么這個(gè)變量就具有了可見性的性質(zhì)了。這就是volatile關(guān)鍵的作用之一了。
volatile保證變量可見性的原理
當(dāng)一個(gè)變量被聲明為volatile時(shí),在編譯成會(huì)變指令的時(shí)候,會(huì)多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);這句指令的意思就是在寄存器執(zhí)行一個(gè)加0的空操作。不過這條指令的前面有一個(gè)lock(鎖)前綴。
當(dāng)處理器在處理擁有l(wèi)ock前綴的指令時(shí):
在之前的處理中,lock會(huì)導(dǎo)致傳輸數(shù)據(jù)的總線被鎖定,其他處理器都不能訪問總線,從而保證處理lock指令的處理器能夠獨(dú)享操作數(shù)據(jù)所在的內(nèi)存區(qū)域,而不會(huì)被其他處理所干擾。
但由于總線被鎖住,其他處理器都會(huì)被堵住,從而影響了多處理器的執(zhí)行效率。為了解決這個(gè)問題,在后來的處理器中,處理器遇到lock指令時(shí)不會(huì)再鎖住總線,而是會(huì)檢查數(shù)據(jù)所在的內(nèi)存區(qū)域,如果該數(shù)據(jù)是在處理器的內(nèi)部緩存中,則會(huì)鎖定此緩存區(qū)域,處理完后把緩存寫回到主存中,并且會(huì)利用緩存一致性協(xié)議來保證其他處理器中的緩存數(shù)據(jù)的一致性。
緩存一致性協(xié)議
剛才我在說可見性的時(shí)候,說“如果一個(gè)共享變量被一個(gè)線程修改了之后,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候,最終會(huì)去內(nèi)存中讀取,而不是從自己的工作空間中讀取”,實(shí)際上是這樣的:
線程中的處理器會(huì)一直在總線上嗅探其內(nèi)部緩存中的內(nèi)存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內(nèi)存地址中的值,而該內(nèi)存地址剛好也在自己的內(nèi)部緩存中,那么處理器就會(huì)強(qiáng)制讓自己對(duì)該緩存地址的無效。所以當(dāng)該處理器要訪問該數(shù)據(jù)的時(shí)候,由于發(fā)現(xiàn)自己緩存的數(shù)據(jù)無效了,就會(huì)去主存中訪問。
有序性
實(shí)際上,當(dāng)我們把代碼寫好之后,虛擬機(jī)不一定會(huì)按照我們寫的代碼的順序來執(zhí)行。例如對(duì)于下面的兩句代碼:
int a = 1; int b = 2;對(duì)于這兩句代碼,你會(huì)發(fā)現(xiàn)無論是先執(zhí)行a = 1還是執(zhí)行b = 2,都不會(huì)對(duì)a,b最終的值造成影響。所以虛擬機(jī)在編譯的時(shí)候,是有可能把他們進(jìn)行重排序的。
為什么要進(jìn)行重排序呢?
你想啊,假如執(zhí)行 int a = 1這句代碼需要100ms的時(shí)間,但執(zhí)行int b = 2這句代碼需要1ms的時(shí)間,并且先執(zhí)行哪句代碼并不會(huì)對(duì)a,b最終的值造成影響。那當(dāng)然是先執(zhí)行int b = 2這句代碼了。
所以,虛擬機(jī)在進(jìn)行代碼編譯優(yōu)化的時(shí)候,對(duì)于那些改變順序之后不會(huì)對(duì)最終變量的值造成影響的代碼,是有可能將他們進(jìn)行重排序的。
更多代碼編譯優(yōu)化可以看我寫的另一篇文章:
虛擬機(jī)在運(yùn)行期對(duì)代碼的優(yōu)化策略
那么重排序之后真的不會(huì)對(duì)代碼造成影響嗎?
實(shí)際上,對(duì)于有些代碼進(jìn)行重排序之后,雖然對(duì)變量的值沒有造成影響,但有可能會(huì)出現(xiàn)線程安全問題的。具體請(qǐng)看下面的代碼
這段代碼最終打印的一定是42嗎?如果沒有重排序的話,打印的確實(shí)會(huì)是42,但如果number = 42和ready = true被進(jìn)行了重排序,顛倒了順序,那么就有可能打印出0了,而不是42。(因?yàn)閚umber的初始值會(huì)是0).
因此,重排序是有可能導(dǎo)致線程安全問題的。
如果一個(gè)變量被聲明volatile的話,那么這個(gè)變量不會(huì)被進(jìn)行重排序,也就是說,虛擬機(jī)會(huì)保證這個(gè)變量之前的代碼一定會(huì)比它先執(zhí)行,而之后的代碼一定會(huì)比它慢執(zhí)行。
例如把上面中的number聲明為volatile,那么number = 42一定會(huì)比ready = true先執(zhí)行。
不過這里需要注意的是,虛擬機(jī)只是保證這個(gè)變量之前的代碼一定比它先執(zhí)行,但并沒有保證這個(gè)變量之前的代碼不可以重排序。之后的也一樣。
volatile關(guān)鍵字能夠保證代碼的有序性,這個(gè)也是volatile關(guān)鍵字的作用。
總結(jié)一下,一個(gè)被volatile聲明的變量主要有以下兩種特性保證保證線程安全。
volatile真的能完全保證一個(gè)變量的線程安全嗎?
我們通過上面的講解,發(fā)現(xiàn)volatile關(guān)鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。
那么,它真的能夠保證一個(gè)變量在多線程環(huán)境下都能被正確的使用嗎?
答案是否定的。原因是因?yàn)镴ava里面的運(yùn)算并非是原子操作。
原子操作
原子操作:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
也就是說,處理器要嘛把這組操作全部執(zhí)行完,中間不允許被其他操作所打斷,要嘛這組操作不要執(zhí)行。
剛才說Java里面的運(yùn)行并非是原子操作。我舉個(gè)例子,例如這句代碼
處理器在處理代碼的時(shí)候,需要處理以下三個(gè)操作:
而這三個(gè)操作處理器是不一定就會(huì)連續(xù)執(zhí)行的,有可能執(zhí)行了第一個(gè)操作之后,處理器就跑去執(zhí)行別的操作的。
證明volatile無法保證線程安全的例子
由于Java中的運(yùn)算并非是原子操作,所以導(dǎo)致volatile聲明的變量無法保證線程安全。
對(duì)于這句話,我給大家舉個(gè)例子。代碼如下:
最終的打印結(jié)果會(huì)是1000 * 10 = 10000嗎?答案是否定的。
問題就出現(xiàn)在t = t + 1這句代碼中。我們來分析一下
例如:
線程1讀取了t的值,假如t = 0。之后線程2讀取了t的值,此時(shí)t = 0。
然后線程1執(zhí)行了加1的操作,此時(shí)t = 1。但是這個(gè)時(shí)候,處理器還沒有把t = 1的值寫回主存中。這個(gè)時(shí)候處理器跑去執(zhí)行線程2,注意,剛才線程2已經(jīng)讀取了t的值,所以這個(gè)時(shí)候并不會(huì)再去讀取t的值了,所以此時(shí)t的值還是0,然后線程2執(zhí)行了對(duì)t的加1操作,此時(shí)t =1 。
這個(gè)時(shí)候,就出現(xiàn)了線程安全問題了,兩個(gè)線程都對(duì)t執(zhí)行了加1操作,但t的值卻是1。所以說,volatile關(guān)鍵字并不一定能夠保證變量的安全性。
什么情況下volatile能夠保證線程安全
剛才雖然說,volatile關(guān)鍵字不一定能夠保證線程安全的問題,其實(shí),在大多數(shù)情況下volatile還是可以保證變量的線程安全問題的。所以,在滿足以下兩個(gè)條件的情況下,volatile就能保證變量的線程安全問題:
總結(jié)
以上是生活随笔為你收集整理的java:彻底搞懂volatile关键字的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。