日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

漫画:什么是volatile关键字?(整合版)

發布時間:2023/12/3 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 漫画:什么是volatile关键字?(整合版) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉載自?永遠愛大家的 程序員小灰

————— 第二天 —————

————————————

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

Java內存模型長成什么樣子呢?就是下圖的樣子:

這里需要解釋幾個概念:

1.主內存(Main Memory)

主內存可以簡單理解為計算機當中的內存,但又不完全等同。主內存被所有的線程所共享,對于一個共享變量(比如靜態變量,或是堆內存中的實例)來說,主內存當中存儲了它的“本尊”。

2.工作內存(Working Memory)

工作內存可以簡單理解為計算機當中的CPU高速緩存,但又不完全等同。每一個線程擁有自己的工作內存,對于一個共享變量來說,工作內存當中存儲了它的“副本”。

線程對共享變量的所有操作都必須在工作內存進行,不能直接讀寫主內存中的變量。不同線程之間也無法訪問彼此的工作內存,變量值的傳遞只能通過主內存來進行。

以上說的這些可能有點抽象,大家來看看下面這個例子:

對于一個靜態變量

static int s = 0;

線程A執行如下代碼:

s = 3;

那么,JMM的工作流程如下圖所示:

通過一系列內存讀寫的操作指令(JVM內存模型共定義了8種內存操作指令,以后會細講),線程A把靜態變量 s=0 從主內存讀到工作內存,再把 s=3 的更新結果同步到主內存當中。從單線程的角度來看,這個過程沒有任何問題。

這時候我們引入線程B,執行如下代碼:

System.out.println("s=" + s);

引入線程B以后,當線程A首先執行,更大的可能是出現下面情況:

此時線程B從主內存得到的s值是3,理所當然輸出 s=3,這種情況不難理解。但是,有較小的幾率出現另一種情況:

因為工作內存所更新的變量并不會立即同步到主內存,所以雖然線程A在工作內存當中已經把變量s的值更新成3,但是線程B從主內存得到的變量s的值仍然是0,從而輸出 s=0。

volatile關鍵字具有許多特性,其中最重要的特性就是保證了用volatile修飾的變量對所有線程的可見性

這里的可見性是什么意思呢?當一個線程修改了變量的值,新的值會立刻同步到主內存當中。而其他線程讀取這個變量的時候,也會從主內存中拉取最新的變量值。

為什么volatile關鍵字可以有這樣的特性?這得益于java語言的先行發生原則(happens-before)。先行發生原則在維基百科上的定義如下:

In computer science, the happened-before relation is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow).

翻譯結果如下:

在計算機科學中,先行發生原則是兩個事件的結果之間的關系,如果一個事件發生在另一個事件之前,結果必須反映,即使這些事件實際上是亂序執行的(通常是優化程序流程)。

這里所謂的事件,實際上就是各種指令操作,比如讀操作、寫操作、初始化操作、鎖操作等等。

先行發生原則作用于很多場景下,包括同步鎖、線程啟動、線程終止、volatile。我們這里只列舉出volatile相關的規則:

對于一個volatile變量的寫操作先行發生于后面對這個變量的讀操作。

回到上述的代碼例子,如果在靜態變量s之前加上volatile修飾符:

volatile static int s = 0;

線程A執行如下代碼:

s = 3;

這時候我們引入線程B,執行如下代碼:

System.out.println("s=" + s);

當線程A先執行的時候,把s = 3寫入主內存的事件必定會先于讀取s的事件。所以線程B的輸出一定是s = 3。

這段代碼是什么意思呢?很簡單,開啟10個線程,每個線程當中讓靜態變量count自增100次。執行之后會發現,最終count的結果值未必是1000,有可能小于1000

使用volatile修飾的變量,為什么并發自增的時候會出現這樣的問題呢?這是因為count++這一行代碼本身并不是原子性操作,在字節碼層面可以拆分成如下指令:

getstatic //讀取靜態變量(count)

iconst_1 //定義常量1

iadd //count增加1

putstatic //把count結果同步到主內存

雖然每一次執行 getstatic 的時候,獲取到的都是主內存的最新變量值,但是進行iadd的時候,由于并不是原子性操作,其他線程在這過程中很可能讓count自增了很多次。這樣一來本線程所計算更新的是一個陳舊的count值,自然無法做到線程安全:

因此,什么時候適合用volatile呢?

1.運行結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。

2.變量不需要與其他的狀態變量共同參與不變約束。

第一條很好理解,就是上面的代碼例子。第二條是什么意思呢?可以看看下面這個場景:

volatile static int start = 3;

volatile static int end = 6;

線程A執行如下代碼:

while (start < end){

//do something

}

線程B執行如下代碼:

start+=3;

end+=3;

這種情況下,一旦在線程A的循環中執行了線程B,start有可能先更新成6,造成了一瞬間 start == end,從而跳出while循環的可能性。

什么是指令重排?

指令重排是指JVM在編譯Java代碼的時候,或者CPU在執行JVM字節碼的時候,對現有的指令順序進行重新排序。

指令重排的目的是為了在不改變程序執行結果的前提下,優化程序的運行效率。需要注意的是,這里所說的不改變執行結果,指的是不改變單線程下的程序執行結果。

然而,指令重排是一把雙刃劍,雖然優化了程序的執行效率,但是在某些情況下,會影響到多線程的執行結果。我們來看看下面的例子:

在線程B中執行:

while( ! contextReady ){

sleep(200);

}

doAfterContextReady (context);

以上程序看似沒有問題。線程B循環等待上下文context的加載,一旦context加載完成,contextReady == true的時候,才執行doAfterContextReady 方法。

但是,如果線程A執行的代碼發生了指令重排,初始化和contextReady的賦值交換了順序:

在線程B中執行:

while( ! contextReady ){

sleep(200);

}

doAfterContextReady (context);

這個時候,很可能context對象還沒有加載完成,變量contextReady 已經為true,線程B直接跳出了循環等待,開始執行doAfterContextReady 方法,結果自然會出現錯誤。

需要注意的是,這里java代碼的重排只是為了簡單示意,真正的指令重排是在字節碼指令的層面。

什么是內存屏障?

內存屏障(Memory Barrier)是一種CPU指令,維基百科給出了如下定義:

A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a CPU or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier.

翻譯結果如下:

內存屏障也稱為內存柵欄或柵欄指令,是一種屏障指令,它使CPU或編譯器對屏障指令之前和之后發出的內存操作執行一個排序約束。 這通常意味著在屏障之前發布的操作被保證在屏障之后發布的操作之前執行。

內存屏障共分為四種類型:

LoadLoad屏障

抽象場景:Load1; LoadLoad; Load2

Load1 和 Load2 代表兩條讀取指令。在Load2要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。

StoreStore屏障:

抽象場景:Store1; StoreStore; Store2

Store1 和 Store2代表兩條寫入指令。在Store2寫入執行前,保證Store1的寫入操作對其它處理器可見

LoadStore屏障:

抽象場景:Load1; LoadStore; Store2

在Store2被寫入前,保證Load1要讀取的數據被讀取完畢。

StoreLoad屏障:

抽象場景:Store1; StoreLoad; Load2

在Load2讀取操作執行前,保證Store1的寫入對所有處理器可見。StoreLoad屏障的開銷是四種屏障中最大的。

volatile做了什么?

在一個變量被volatile修飾后,JVM會為我們做兩件事:

1.在每個volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障。

2.在每個volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障。

或許這樣說有些抽象,我們看一看剛才線程A代碼的例子:

我們給contextReady 增加volatile修飾符,會帶來什么效果呢?

context = loadContext() 和屏障下方的volatile寫入語句 contextReady = true 無法交換順序,從而成功阻止了指令重排序。

volatile特性之一:

保證變量在線程之間的可見性。可見性的保證是基于CPU的內存屏障指令,被JSR-133抽象為happens-before原則。

volatile特性之二:

阻止編譯時和運行時的指令重排。編譯時JVM編譯器遵循內存屏障的約束,運行時依靠CPU屏障指令來阻止重排。

幾點補充:

1. 關于volatile的介紹,本文很多內容來自《深入理解Java虛擬機》這本書。有興趣的同學可以去看看。

2. 在使用volatile引入內存屏障的時候,普通讀、普通寫、volatile讀、volatile寫會排列組合出許多不同的場景。我們這里只簡單列出了其中一種,有興趣的同學可以查資料進一步學習其他阻止指令重排的場景。

3.volatile除了保證可見性和阻止指令重排,還解決了long類型和double類型數據的8字節賦值問題。這個特性相對簡單,本文就不詳細描述了。

4.本漫畫純屬娛樂,還請大家盡量珍惜當下的工作,切勿模仿小灰的行為哦。

—————END—————


總結

以上是生活随笔為你收集整理的漫画:什么是volatile关键字?(整合版)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。