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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

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

轉(zhuǎn)載自?永遠(yuǎn)愛大家的 程序員小灰

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

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

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

Java內(nèi)存模型長成什么樣子呢?就是下圖的樣子:

這里需要解釋幾個(gè)概念:

1.主內(nèi)存(Main Memory)

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

2.工作內(nèi)存(Working Memory)

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

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

以上說的這些可能有點(diǎn)抽象,大家來看看下面這個(gè)例子:

對于一個(gè)靜態(tài)變量

static int s = 0;

線程A執(zhí)行如下代碼:

s = 3;

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

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

這時(shí)候我們引入線程B,執(zhí)行如下代碼:

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

引入線程B以后,當(dāng)線程A首先執(zhí)行,更大的可能是出現(xiàn)下面情況:

此時(shí)線程B從主內(nèi)存得到的s值是3,理所當(dāng)然輸出 s=3,這種情況不難理解。但是,有較小的幾率出現(xiàn)另一種情況:

因?yàn)楣ぷ鲀?nèi)存所更新的變量并不會立即同步到主內(nèi)存,所以雖然線程A在工作內(nèi)存當(dāng)中已經(jīng)把變量s的值更新成3,但是線程B從主內(nèi)存得到的變量s的值仍然是0,從而輸出 s=0。

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

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

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

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).

翻譯結(jié)果如下:

在計(jì)算機(jī)科學(xué)中,先行發(fā)生原則是兩個(gè)事件的結(jié)果之間的關(guān)系,如果一個(gè)事件發(fā)生在另一個(gè)事件之前,結(jié)果必須反映,即使這些事件實(shí)際上是亂序執(zhí)行的(通常是優(yōu)化程序流程)。

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

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

對于一個(gè)volatile變量的寫操作先行發(fā)生于后面對這個(gè)變量的讀操作。

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

volatile static int s = 0;

線程A執(zhí)行如下代碼:

s = 3;

這時(shí)候我們引入線程B,執(zhí)行如下代碼:

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

當(dāng)線程A先執(zhí)行的時(shí)候,把s = 3寫入主內(nèi)存的事件必定會先于讀取s的事件。所以線程B的輸出一定是s = 3。

這段代碼是什么意思呢?很簡單,開啟10個(gè)線程,每個(gè)線程當(dāng)中讓靜態(tài)變量count自增100次。執(zhí)行之后會發(fā)現(xiàn),最終count的結(jié)果值未必是1000,有可能小于1000

使用volatile修飾的變量,為什么并發(fā)自增的時(shí)候會出現(xiàn)這樣的問題呢?這是因?yàn)閏ount++這一行代碼本身并不是原子性操作,在字節(jié)碼層面可以拆分成如下指令:

getstatic //讀取靜態(tài)變量(count)

iconst_1 //定義常量1

iadd //count增加1

putstatic //把count結(jié)果同步到主內(nèi)存

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

因此,什么時(shí)候適合用volatile呢?

1.運(yùn)行結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。

2.變量不需要與其他的狀態(tài)變量共同參與不變約束。

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

volatile static int start = 3;

volatile static int end = 6;

線程A執(zhí)行如下代碼:

while (start < end){

//do something

}

線程B執(zhí)行如下代碼:

start+=3;

end+=3;

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

什么是指令重排?

指令重排是指JVM在編譯Java代碼的時(shí)候,或者CPU在執(zhí)行JVM字節(jié)碼的時(shí)候,對現(xiàn)有的指令順序進(jìn)行重新排序。

指令重排的目的是為了在不改變程序執(zhí)行結(jié)果的前提下,優(yōu)化程序的運(yùn)行效率。需要注意的是,這里所說的不改變執(zhí)行結(jié)果,指的是不改變單線程下的程序執(zhí)行結(jié)果。

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

在線程B中執(zhí)行:

while( ! contextReady ){

sleep(200);

}

doAfterContextReady (context);

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

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

在線程B中執(zhí)行:

while( ! contextReady ){

sleep(200);

}

doAfterContextReady (context);

這個(gè)時(shí)候,很可能context對象還沒有加載完成,變量contextReady 已經(jīng)為true,線程B直接跳出了循環(huán)等待,開始執(zhí)行doAfterContextReady 方法,結(jié)果自然會出現(xiàn)錯(cuò)誤。

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

什么是內(nèi)存屏障?

內(nèi)存屏障(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.

翻譯結(jié)果如下:

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

內(nèi)存屏障共分為四種類型:

LoadLoad屏障

抽象場景:Load1; LoadLoad; Load2

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

StoreStore屏障:

抽象場景:Store1; StoreStore; Store2

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

LoadStore屏障:

抽象場景:Load1; LoadStore; Store2

在Store2被寫入前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。

StoreLoad屏障:

抽象場景:Store1; StoreLoad; Load2

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

volatile做了什么?

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

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

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

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

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

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

volatile特性之一:

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

volatile特性之二:

阻止編譯時(shí)和運(yùn)行時(shí)的指令重排。編譯時(shí)JVM編譯器遵循內(nèi)存屏障的約束,運(yùn)行時(shí)依靠CPU屏障指令來阻止重排。

幾點(diǎn)補(bǔ)充:

1. 關(guān)于volatile的介紹,本文很多內(nèi)容來自《深入理解Java虛擬機(jī)》這本書。有興趣的同學(xué)可以去看看。

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

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

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

—————END—————


總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。