計算機在處理數據的過程中爲什麼會出現線程不安全的問題。
計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中會涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。
爲了處理這個問題,在CPU裏面就有了高速緩存(Cache)的概念。當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。
CPU<---->高速緩存<---->內存
比如cpu在執行下面這段代碼的時候,t = t + 1;
會先從高速緩存中查看是否有t的值,如果有,則直接拿來使用,如果沒有,則會從主存中讀取,讀取之後會複製一份存放在高速緩存中方便下次使用。之後cup進行對t加1操作,然後把數據寫入高速緩存,最後會把高速緩存中的數據刷新到主存中。
Java這種語言在處理線程安全問題的時候,會有自己的處理機制,例如volatile關鍵字,synchronized關鍵字,並且這種機制適用於各種平臺。
爲每個線程開啓工作內存。
Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。(線程共享區,線程獨佔區)
由於java中的每個線程有自己的工作空間,這種工作空間相當於上面所說的高速緩存,因此多個線程在處理一個共享變量的時候,就會出現線程安全問題。
共享變量:上面我們所說的t就是一個共享變量,也就是說,能夠被多個線程訪問到的變量,我們稱之爲共享變量。在java中共享變量包括實例變量,靜態變量,數組元素。他們都被存放在堆內存中。
volatile關鍵字是如何保證線程安全問題的?
可見性:假如一個變量被聲明爲volatile,那麼這個變量就具有了可見性的性質了。這就是volatile關鍵的作用之一了。 其他線程能夠知道當前線程對共享變量的修改狀況。
volatile保證變量可見性的原理
在後來的處理器中,處理器遇到lock指令時不會再鎖住總線,而是會檢查數據所在的內存區域,如果該數據是在處理器的內部緩存中,則會鎖定此緩存區域,處理完後把緩存寫回到主存中,並且會利用緩存一致性協議來保證其他處理器中的緩存數據的一致性。
緩存一致性協議
剛纔我在說可見性的時候,說“如果一個共享變量被一個線程修改了之後,當其他線程要讀取這個變量的時候,最終會去內存中讀取,而不是從自己的工作空間中讀取”,實際上是這樣的:
線程中的處理器會一直在總線上嗅探其內部緩存中的內存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內存地址中的值,而該內存地址剛好也在自己的內部緩存中,那麼處理器就會強制讓自己對該緩存地址的無效。所以當該處理器要訪問該數據的時候,由於發現自己緩存的數據無效了,就會去主存中訪問。(強制緩存無效,從主存中取訪問)
有序性
實際上,當我們把代碼寫好之後,虛擬機不一定會按照我們寫的代碼的順序來執行。例如對於下面的兩句代碼:
int a = 1; int b = 2;
對於這兩句代碼,你會發現無論是先執行a = 1還是執行b = 2,都不會對a,b最終的值造成影響。所以虛擬機在編譯的時候,是有可能把他們進行重排序的。
爲什麼要進行重排序呢?
假如執行 int a = 1這句代碼需要100ms的時間,但執行int b = 2這句代碼需要1ms的時間,並且先執行哪句代碼並不會對a,b最終的值造成影響。那當然是先執行int b = 2這句代碼了。
所以,虛擬機在進行代碼編譯優化的時候,對於那些改變順序之後不會對最終變量的值造成影響的代碼,是有可能將他們進行重排序的。
如果一個變量被聲明volatile的話,那麼這個變量不會被進行重排序,也就是說,虛擬機會保證這個變量之前的代碼一定會比它先執行,而之後的代碼一定會比它慢執行。
例如把上面中的number聲明爲volatile,那麼number = 42一定會比ready = true先執行。
volatile關鍵字能夠保證代碼的有序性,這個也是volatile關鍵字的作用。
總結一下,一個被volatile聲明的變量主要有以下兩種特性保證保證線程安全: 可見性, 有序性。
我們通過上面的講解,發現volatile關鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。
那麼,它真的能夠保證一個變量在多線程環境下都能被正確的使用嗎?
答案是否定的。原因是因爲Java裏面的運算並非是原子操作。