摘要:此時,就出現了線程不安全問題了。因為的初始值會是因此,重排序是有可能導致線程安全問題的。真的能完全保證一個變量的線程安全嗎我們通過上面的講解,發現關鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。
對于volatile這個關鍵字,相信很多朋友都聽說過,甚至使用過,這個關鍵字雖然字面上理解起來比較簡單,但是要用好起來卻不是一件容易的事。
這篇文章將從多個方面來講解volatile,讓你對它更加理解。
計算機中為什么會出現線程不安全的問題volatile既然是與線程安全有關的問題,那我們先來了解一下計算機在處理數據的過程中為什么會出現線程不安全的問題。
大家都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中會涉及到數據的讀取和寫入。由于程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由于CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。
為了處理這個問題,在CPU里面就有了高速緩存(Cache)的概念。當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。
我舉個簡單的例子,比如cpu在執行下面這段代碼的時候,
t = t + 1;
會先從高速緩存中查看是否有t的值,如果有,則直接拿來使用,如果沒有,則會從主存中讀取,讀取之后會復制一份存放在高速緩存中方便下次使用。之后cup進行對t加1操作,然后把數據寫入高速緩存,最后會把高速緩存中的數據刷新到主存中。
這一過程在單線程運行是沒有問題的,但是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行于不同的CPU中,因此每個線程運行時有自己的高速緩存(對單核CPU來說,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的,本次講解以多核cup為主)。這時就會出現同一個變量在兩個高速緩存中的值不一致問題了。
例如:
兩個線程分別讀取了t的值,假設此時t的值為0,并且把t的值存到了各自的高速緩存中,然后線程1對t進行了加1操作,此時t的值為1,并且把t的值寫回到主存中。但是線程2中高速緩存的值還是0,進行加1操作之后,t的值還是為1,然后再把t的值寫回主存。
此時,就出現了線程不安全問題了。
Java中的線程安全問題上面那種線程安全問題,可能對于不同的操作系統會有不同的處理機制,例如Windows操作系統和Linux的操作系統的處理方法可能會不同。
我們都知道,Java是一種夸平臺的語言,因此Java這種語言在處理線程安全問題的時候,會有自己的處理機制,例如volatile關鍵字,synchronized關鍵字,并且這種機制適用于各種平臺。
Java內存模型規定所有的變量都是存在主存當中(類似于前面說的物理內存),每個線程都有自己的工作內存(類似于前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。并且每個線程不能訪問其他線程的工作內存。
由于java中的每個線程有自己的工作空間,這種工作空間相當于上面所說的高速緩存,因此多個線程在處理一個共享變量的時候,就會出現線程安全問題。
這里簡單解釋下共享變量,上面我們所說的t就是一個共享變量,也就是說,能夠被多個線程訪問到的變量,我們稱之為共享變量。在java中共享變量包括實例變量,靜態變量,數組元素。他們都被存放在堆內存中。volatile關鍵字
上面扯了一大堆,都沒提到volatile關鍵字的作用,下面開始講解volatile關鍵字是如何保證線程安全問題的。
可見性意思就是說,在多線程環境下,某個共享變量如果被其中一個線程給修改了,其他線程能夠立即知道這個共享變量已經被修改了,當其他線程要讀取這個變量的時候,最終會去內存中讀取,而不是從自己的工作空間中讀取
例如我們上面說的,當線程1對t進行了加1操作并把數據寫回到主存之后,線程2就會知道它自己工作空間內的t已經被修改了,當它要執行加1操作之后,就會去主存中讀取。這樣,兩邊的數據就能一致了。
假如一個變量被聲明為volatile,那么這個變量就具有了可見性的性質了。這就是volatile關鍵的作用之一了。
當一個變量被聲明為volatile時,在編譯成會變指令的時候,會多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);
這句指令的意思就是在寄存器執行一個加0的空操作。不過這條指令的前面有一個lock(鎖)前綴。
當處理器在處理擁有lock前綴的指令時:
在之前的處理中,lock會導致傳輸數據的總線被鎖定,其他處理器都不能訪問總線,從而保證處理lock指令的處理器能夠獨享操作數據所在的內存區域,而不會被其他處理所干擾。
但由于總線被鎖住,其他處理器都會被堵住,從而影響了多處理器的執行效率。為了解決這個問題,在后來的處理器中,處理器遇到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這句代碼了。
所以,虛擬機在進行代碼編譯優化的時候,對于那些改變順序之后不會對最終變量的值造成影響的代碼,是有可能將他們進行重排序的。
更多代碼編譯優化可以看我寫的另一篇文章:
虛擬機在運行期對代碼的優化策略
那么重排序之后真的不會對代碼造成影響嗎?
實際上,對于有些代碼進行重排序之后,雖然對變量的值沒有造成影響,但有可能會出現線程安全問題的。具體請看下面的代碼
public class NoVisibility{ private static boolean ready; private static int number; private static class Reader extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); } } public static void main(String[] args){ new Reader().start(); number = 42; ready = true; } }
這段代碼最終打印的一定是42嗎?如果沒有重排序的話,打印的確實會是42,但如果number = 42和ready = true被進行了重排序,顛倒了順序,那么就有可能打印出0了,而不是42。(因為number的初始值會是0).
因此,重排序是有可能導致線程安全問題的。
如果一個變量被聲明volatile的話,那么這個變量不會被進行重排序,也就是說,虛擬機會保證這個變量之前的代碼一定會比它先執行,而之后的代碼一定會比它慢執行。
例如把上面中的number聲明為volatile,那么number = 42一定會比ready = true先執行。
不過這里需要注意的是,虛擬機只是保證這個變量之前的代碼一定比它先執行,但并沒有保證這個變量之前的代碼不可以重排序。之后的也一樣。
volatile關鍵字能夠保證代碼的有序性,這個也是volatile關鍵字的作用。
總結一下,一個被volatile聲明的變量主要有以下兩種特性保證保證線程安全。
可見性。
有序性。
volatile真的能完全保證一個變量的線程安全嗎?我們通過上面的講解,發現volatile關鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。
那么,它真的能夠保證一個變量在多線程環境下都能被正確的使用嗎?
答案是否定的。原因是因為Java里面的運算并非是原子操作。
原子操作:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
也就是說,處理器要嘛把這組操作全部執行完,中間不允許被其他操作所打斷,要嘛這組操作不要執行。
剛才說Java里面的運行并非是原子操作。我舉個例子,例如這句代碼
int a = b + 1;
處理器在處理代碼的時候,需要處理以下三個操作:
從內存中讀取b的值。
進行a = b + 1這個運算
把a的值寫回到內存中
而這三個操作處理器是不一定就會連續執行的,有可能執行了第一個操作之后,處理器就跑去執行別的操作的。
由于Java中的運算并非是原子操作,所以導致volatile聲明的變量無法保證線程安全。
對于這句話,我給大家舉個例子。代碼如下:
public class Test{ public static volatile int t = 0; public static void main(String[] args){ Thread[] threads = new Thread[10]; for(int i = 0; i < 10; i++){ //每個線程對t進行1000次加1的操作 threads[i] new Thread(new Runnable(){ @Override public void run(){ for(int j = 0; j < 1000; j++){ t = t + 1; } } }); threads[i].start(); } //等待所有累加線程都結束 while(Thread.activeCount() > 1){ Thread.yield(); } //打印t的值 System.out.println(t); } }
最終的打印結果會是1000 * 10 = 10000嗎?答案是否定的。
問題就出現在t = t + 1這句代碼中。我們來分析一下
例如:
線程1讀取了t的值,假如t = 0。之后線程2讀取了t的值,此時t = 0。
然后線程1執行了加1的操作,此時t = 1。但是這個時候,處理器還沒有把t = 1的值寫回主存中。這個時候處理器跑去執行線程2,注意,剛才線程2已經讀取了t的值,所以這個時候并不會再去讀取t的值了,所以此時t的值還是0,然后線程2執行了對t的加1操作,此時t =1 。
這個時候,就出現了線程安全問題了,兩個線程都對t執行了加1操作,但t的值卻是1。所以說,volatile關鍵字并不一定能夠保證變量的安全性。
什么情況下volatile能夠保證線程安全剛才雖然說,volatile關鍵字不一定能夠保證線程安全的問題,其實,在大多數情況下volatile還是可以保證變量的線程安全問題的。所以,在滿足以下兩個條件的情況下,volatile就能保證變量的線程安全問題:
運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
變量不需要與其他狀態變量共同參與不變約束。
講到這里,關于volatile關鍵字的就算講完了。如果有哪里講的不對的地方,非常歡迎你的指點。下篇應該會講synchronize關鍵字。
完
參考書籍:
深入理解Java虛擬機(JVM高級特性與最佳實踐)。
Java并非編程實戰
關注公眾號:苦逼的碼農,獲取更多原創文章,后臺回復"禮包"送你一份資源大禮包。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/76902.html
摘要:撤銷鎖偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。輕量級鎖線程在執行同步代碼塊之前,會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的復制到鎖記錄中,官方稱為。 前言 作者前面也寫了幾篇關于Java并發編程,以及線程和volatil的基礎知識,有興趣可以閱讀作者的原文博客,今天關于Java中的兩種鎖進行詳解,希望對...
摘要:單例模式是一種常用的設計模式也可能是設計模式中代碼量最少的設計模式。簡介單例模式屬于中設計模式中的創建型模式定義是確保某一個類只有一個實例并提供一個全局的訪問點。 單例模式是一種常用的設計模式、也可能是設計模式中代碼量最少的設計模式。但是少并不意味著簡單、想要用好、用對單例、就的費一番腦子了。因為它里面涉及到了很多Java底層的知識如類裝載機制、Java內存模型、volatile等知識...
摘要:接著線程過來通過方式獲取鎖,獲取鎖的過程就是通過操作變量將其值從變為。線程加鎖成功后還有一步重要的操作,就是將設置成為自己。線程屁顛屁顛的就去等待區小憩一會去了。 一、寫在前面 這篇文章,我們聊一聊Java并發中的核武器, AQS底層實現。 不管是工作三四年、還是五六年的在工作或者面試中涉及到并發的是時候總是繞不過AQS這個詞。 首先,確實還有很多人連AQS是什么都不知道,甚至有的竟...
摘要:總結我們主要介紹到了以下幾種方式實現單例模式餓漢方式線程安全懶漢式非線程安全和關鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設計模式中文版第二版設計模式深入理解單例模式我是一個以架構師為年之內目標的小小白。 初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的了解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。...
閱讀 2021·2021-08-21 14:09
閱讀 485·2019-08-30 15:44
閱讀 2112·2019-08-29 16:32
閱讀 1376·2019-08-29 15:36
閱讀 3442·2019-08-29 12:43
閱讀 2784·2019-08-29 11:14
閱讀 435·2019-08-28 18:26
閱讀 2253·2019-08-26 13:57