volatile說明

本文簡敘:

1.詭異的死循環

2.java內存模型簡述及JMM對volatile變量定義的特殊規則

3.通過與synchronized對比來進一步說明volatile作用

一:詭異的死循環

在說volatile之前,先來個小例子來引入今天得話題。

 

圖1

   如圖1,在eclipse jdk764位環境下執行以上代碼會出現詭異得死循環。結果如右圖所示。看今天得話題也知道只要在4行代碼處添加volatile關鍵字就能解決這死循環問題(可以自己實驗下,此處就不貼出了)。是什麼原因造成得這個現象呢?

    在第9行出執行setIsFlag 方法,在第10行處也的確輸出isFlag爲 false,然而意外的是代碼卻卡在了22行循環處,要知道這裏爲什麼會這樣我們還得先說說java內存模型。

二:java內存模型簡述及JMM對volatile變量定義的特殊規則

            Java內存模型簡稱JMM(Java Memory Model),是Java虛擬機所定義的一種抽象規範,用來屏蔽不同硬件和操作系統的內存訪問差異,讓java程序在各種平臺下都能達到一致的內存訪問效果。JMM長什麼樣呢,如下圖。

            Java內存模型的主要目標是定義Java內存中各個變量的訪問規則,即在虛擬機中把變量存入內存和在內存中取出變量這樣的底層細節。此處的變量與java編程所說的變量有所區別,它包括實例字段,靜態字段和構成數組對象的元素,不包括局部變量和方法參數。後者是線程私有的不存在共享問題,自然也就不存在競爭問題(此處應注意區分概念,如果局部變量是一個reference(引用)類型,它引用的對象存在堆中是可以被各個線程所共享的,而reference本身是在java棧的局部變量表中,它是線程私有的)。

    從抽象角度看,JMM定義了java線程和主線程之間的抽象關係:線程之間的共享變量存儲在主內存,每一個線程都有一個私有的本地內存,本地內存中存儲了以讀/寫共享變量的副本(這裏值得注意的是,要是一個共享變量爲對象,並不會把整個對象在copy一份,這個對象的引用,對象中的某個字段有可能會被拷貝一份)。本地內存時抽象的一個概念,並不真實存在。它涵蓋了緩存,寫緩衝,寄存器以及其他的硬件和編譯器的優化。

            Java內存模型並沒有限制執行引擎使用處理器的特點寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器驚喜調整代碼執行順序這類優化措施。

            關於主內存與工作內存之間的交互協議,即一個變量如何從主內存拷貝到工作內存。如何從工作內存同步到主內存中的實現細節。java內存模型定義了8種操作來完成。這8種操作每一種都是原子操作。8種操作如下

· lock(鎖定):作用於主內存,它把一個變量標記爲一條線程獨佔狀態;

· unlock(解鎖):作用於主內存,它將一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其他線程鎖定;

· read(讀取):作用於主內存,它把變量值從主內存傳送到線程的工作內存中,以便隨後的load動作使用;

· load(載入):作用於工作內存,它把read操作的值放入工作內存中的變量副本中;

· use(使用):作用於工作內存,它把工作內存中的值傳遞給執行引擎,每當虛擬機遇到一個需要使用這個變量的指令時候,將會執行這個動作;

· assign(賦值):作用於工作內存,它把從執行引擎獲取的值賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的指令時候,執行該操作;

· store(存儲):作用於工作內存,它把工作內存中的一個變量傳送給主內存中,以備隨後的write操作使用;

· write(寫入):作用於主內存,它把store傳送值放到主內存中的變量中。

Java內存模型還規定了執行上述8種基本操作時必須滿足如下規則:

   1 · 不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。

   2· 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。

   3 · 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。

   4 · 一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。

   5 · 一個變量在同一個時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。

    6· 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

    7· 如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。

    8· 對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)。

    我們來說下java內存模型對volatile變量定義的特殊規則。假定T表示一個線程,v和w分別表示兩個volatile型變量,那麼在進行read/load/use/assign/store/write操作時需要滿足如下規則(來自深入理解java虛擬機):

1. 只有當線程T對變量V執行的前一個動作是load的時候,線程T才能對變量V執行use動作,並且,只有當線程T對變量V執行的後一個動作是use的時候,線程T才能對變量V執行load動作。線程T對變量V的use動作可以認爲是和線程T對變量V的load/read動作相關聯,必須連續一起出現(這條規則要求在工作內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其他線程對變量V所做的修改後的值。這條翻譯下就是read->load->use 這幾個指令以原子形式執行)。

2. 只有當線程T對變量V執行的前一個動作的是assign的時候,線程T才能對變量V執行store動作;並且,只有當線程T對變量V執行的後一個動作是store的時候,線程T才能對變量V執行assign動作。線程T對變量V的assign動作可以認爲是和線程T對變量V的store,write動作相關聯,必須聯繫一起出現(這條規則要求在工作內存中,每次修改V後都必須立刻同步回主內存中,用於保證其他線程可以看到自己對變量V所做的修改。這條翻譯下就是 assign->store->write 這幾個指令以原子的形式執行注:綜合1,2條可以通俗的理解成JMM把本地內存設成爲無效

3.假定動作A是線程T對變量V實施的use或assign動作,假定動作F是和動作A相關聯的load或store動作,假定動作P是和動作F相應的對應變量V的read或write動作;類似的,假定動作B是線程T對變量W實施的use或assign動作,假定動作G是和動作B相關聯的load或store動作,假定動作Q是和動作G想要的對變量W的read或write動作。如果A先於B,那麼P先於Q(這條規則要求volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同。這條請忽略,太繞口了,下文會詳細講volatile對重排序的影響)

絮絮叨叨一大堆,但是內存模型又是不可迴避的話題。現在回到剛纔死循環的問題,在啓動RunThread線程後,該線程把public static boolean isFlag = true讀取到該線程的本地內存中,然後while每次循環讀取的值都是在自己的本地內存中取得isFlag=true,即便是主線程更改了主內存中的isFlag 值,所以該線程死循環了。這個問題其實就是私有堆棧的值和公共堆棧中的值不同步造成的。解決該問題的辦法就是用volatile聲明isFlag。基本都知道volatile有兩個功能:保證內存可見性(強制從主內存讀值),禁止指令重拍。這裏就是用到它的保證內存的可見性,在isflag聲明爲volatile後上文中的RunThread線程在每次while循環的時候就會直接去主內存讀值。

其實這裏基本就把volatile第一個作用基本介紹差不多了(保證內存可見性)。不過只是介紹到這估計不少同學會矇蔽來,矇蔽着去~~

三.通過與synchronized對比來進一步說明volatile作用

synchronized這個關鍵字相信大家都相對熟悉,並且相信不少同學會把這兩個關鍵字混餚。爲了更好的理解volatile這裏就說下synchronized和volatile的不同:

1. 關鍵字volatile性能別synchronized好,並且volatile只能修飾變量,而synchronized可以修飾方法,代碼塊。

2. 多線程下volatile不會發生阻塞,而synchronized會。

3. Volatile能保證數據的可見性,但不能保證原子性;而synchronized可以保證原子性並間接的保證可見性。

爲了更直白的的來說明volatile,下面通過具體的代碼實例來說明Volatile的作用及與synchronized的區別

 

假設有多個線程調用上面三個方法,這個程序在語意上和下面等價。

 

如上面實例程序所示,一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作都是使用同一個鎖來同步,它們之間執行效果相同。

簡而言之,volatile變量自身具有下列特性:

1. 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。

2. 原子性。對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

讓我們從內存方面進一步的來分析下在多線程環境下像volatile int count=;count++ 這種操作爲什麼不是原子性的。見下圖

 

上圖中,操作2不是原子操作(原因見上文,JMM對volatile聲明的變量只保證了操作1和操作3的原子性),並且這一步在多線程中是多次出現的,比如線程1在執行操作2,而線程2又去讀了主線程的count或者線程2已經執行完操作3把主內存中的count值覆蓋了等等。。這樣都會出現問題的。volatile變量只保證了從內存中讀的值是最新值(具體原因上文多次講到),並不能保證一個變量複合操作的原子性,所以一個變量的複合操作還是用同步鎖吧。

寫在這一小結的後邊:對於引用型 volatile 變量, volatile 關鍵字只是保證讀線程能夠讀取到個指向對象的相對新的內存地址(引用),而這個內存地址指向的對象的實例/靜態變量值是否是相對新的則沒有保障。

ps:文中內容大部分參考自<<深入理解java虛擬機(第二版)>>,<<java併發編程藝術>>,<<Java多線程編程實戰指南 核心篇>>,<<Java多線程編程核心技術>>幾本書,有興趣的小夥伴可以直接翻閱原書。

轉載請表明出處:https://blog.csdn.net/qq_31387317/article/details/80449467

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章