自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入理解 Volatile 關(guān)鍵字

開(kāi)發(fā) 前端
在本文中,我們將討論與同步相關(guān)的一些不好的做法,以及針對(duì)每個(gè)使用情況的更好的方法。

volatile 關(guān)鍵字是 Java 語(yǔ)言的高級(jí)特性,但要弄清楚其工作原理,需要先弄懂 Java 內(nèi)存模型。

初學(xué) volatile 關(guān)鍵字,我們需要弄清楚它到底意味著什么??偟膩?lái)說(shuō),它有兩個(gè)含義,分別是:

  • 保證可見(jiàn)性
  • 禁止指令重排序

保證可見(jiàn)性

保證可見(jiàn)性指的是:當(dāng)一個(gè)線程修改了某個(gè)變量時(shí),其他所有線程都知道該變量被修改了。 由于 volatile 可以保證可見(jiàn)性,因此 Java 能夠保證現(xiàn)在在讀取 volatile 變量時(shí),線程讀取到的值是準(zhǔn)確的。但是這并不意味著對(duì) volatile 變量的操作是線程安全的,因?yàn)橛锌赡茉谧x取到變量之后,又有其他線程對(duì)變量進(jìn)行修改了。

為了說(shuō)明這個(gè)問(wèn)題,我們可以舉個(gè)簡(jiǎn)單地例子。下面代碼發(fā)起了 20 個(gè)線程,每個(gè)線程對(duì) race 變量進(jìn)行 1 萬(wàn)次自增操作。如果這段代碼能夠正確并發(fā)執(zhí)行,那么最后輸出的結(jié)果應(yīng)該是 20 萬(wàn)。但實(shí)際上,每次輸出的結(jié)果都不一樣,都是一個(gè)小于 20 萬(wàn)的數(shù)字,為什么呢?

這是因?yàn)楫?dāng)線程在獲取到 race 變量的值,然后對(duì)其進(jìn)行自增這中間,有可能其他線程對(duì) race 變量做了自增操作,然后寫(xiě)回了主內(nèi)存。而當(dāng)前線程再將數(shù)據(jù)寫(xiě)回主內(nèi)存時(shí),就發(fā)生了數(shù)據(jù)覆蓋。因此,就發(fā)生了數(shù)據(jù)不一致的問(wèn)題。

要使得 volatile 變量不發(fā)生并發(fā)安全問(wèn)題,只需要遵守如下兩條規(guī)則即可:

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

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

第一條規(guī)則比較好理解,例如上面例子的 race 變量,其運(yùn)算結(jié)果就依賴(lài)于變量的當(dāng)前值,所以其并不符合第一條規(guī)則,因此就會(huì)有線程安全問(wèn)題。但如果 race++? 變成了 race=1; 這樣的情況,那么 race 的值就不依賴(lài)變量當(dāng)前值,因此就不會(huì)有線程安全問(wèn)題。

第二條規(guī)則有點(diǎn)晦澀難懂。其意思是說(shuō),變量不能和其他變量一起參與判斷,無(wú)論其他變量是否是 volatile 類(lèi)型的變量。例如 if(a && b) 這個(gè)判斷就無(wú)法滿足 volatile 的第二條規(guī)則,會(huì)發(fā)生線程安全問(wèn)題,即使這兩個(gè)變量都是 volatile 類(lèi)型的變量。

關(guān)于第二條規(guī)則的描述,為啥與其他變量一起,就沒(méi)法保證線程安全呢?

要解答這個(gè)問(wèn)題,我們不妨假設(shè)一下各種可能的場(chǎng)景。

我們假設(shè)變量 a b 的初始值都是 true,并且兩者都是 volatile 類(lèi)型變量。

場(chǎng)景一:線程 A 執(zhí)行 if(a && b) 判斷,先判斷變量 a,發(fā)現(xiàn)是 true,于是繼續(xù)判斷變量 b。發(fā)現(xiàn)變量 b 也是 true,于是整個(gè)表達(dá)式為 true。

場(chǎng)景二:線程 A 執(zhí)行 if(a && b) 判斷,先判斷變量 a,發(fā)現(xiàn)是 true。此時(shí)線程 B 修改了變量 b 的值為 false。接著線程 A 繼續(xù)判斷變量 b 的值,發(fā)現(xiàn)變量 b 的值為 false。于是整體表達(dá)式的值為 false。

通過(guò)上面的例子,我們發(fā)現(xiàn)同樣的表達(dá)式在不同的并發(fā)場(chǎng)景下會(huì)有不同的結(jié)果,這很明顯就是線程不安全的。因?yàn)榫€程安全的代碼,在單線程和多線程下,其結(jié)果應(yīng)該是一樣的。

禁止指令重排序

指令重排序,指的是硬件層面為了加快執(zhí)行速度,可能會(huì)調(diào)整指令的執(zhí)行順序,從而會(huì)出現(xiàn)并不按代碼順序的執(zhí)行情況出現(xiàn)。例如下面的代碼里,我們初始化了 flag 變量為 false,然后再將 flag 變量置為 true。但這樣的代碼在并發(fā)執(zhí)行的時(shí)候,有可能先將 flag 職位 true,再將 flag 變?yōu)?false,從而發(fā)生線程安全問(wèn)題。

boolean flag = false;
flag = true;

我們說(shuō) volatile 變量禁止指令重排序,其實(shí)就是指被 volatile 修飾的變量,其執(zhí)行順序不能被重排序。 禁止重排序的實(shí)現(xiàn),是使用了一個(gè)叫「內(nèi)存屏障」的東西。簡(jiǎn)單地說(shuō),內(nèi)存屏障的作用就是指令重排序時(shí),不能把后面的指令重排序到內(nèi)存屏障之前的位置。

可見(jiàn)性的來(lái)源

我們前面說(shuō)過(guò):volatile 修飾的變量,當(dāng)其被修改之后,其他變量就能立即獲取到其變化。但這個(gè)可見(jiàn)性的來(lái)源是哪里呢?為什么其能夠?qū)崿F(xiàn)這樣的可見(jiàn)性呢?其實(shí) volatile 的這些功能來(lái)源于 Java 內(nèi)存模型中對(duì) volatile 變量定義的特殊規(guī)則。

假定 T 表示一個(gè)線程,V 和 W 分別表示兩個(gè) volatile 型變量。在 Java 內(nèi)存模型中規(guī)定在進(jìn)行 read、load、use、assign、store 和 write 操作時(shí)需要滿足如下規(guī)則:

  • 只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的前一個(gè)動(dòng)作是 load 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 use 動(dòng)作。并且,只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的后一個(gè)動(dòng)作是 use 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 load 動(dòng)作。
  • 只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的前一個(gè)動(dòng)作是 assign 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 store 動(dòng)作;并且,只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的后一個(gè)動(dòng)作是 store 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 assign 動(dòng)作。
  • 假定動(dòng)作 A 是線程 T 對(duì)變量 V 實(shí)施的 use 或 assign 動(dòng)作,假定動(dòng)作 F 是和動(dòng)作 A 相關(guān)聯(lián)的 load 或 store 動(dòng)作,假定動(dòng)作 P 是和動(dòng)作 F 相應(yīng)的對(duì)變量 V 的 read 或 write 動(dòng)作。類(lèi)似的,假定動(dòng)作 B 是線程 T 對(duì)變量 W 實(shí)施的 use 或 assign 動(dòng)作,假定動(dòng)作 G 是和動(dòng)作 B 相關(guān)聯(lián)的 load 或 store 動(dòng)作,假定動(dòng)作 Q 是和動(dòng)作 G 相應(yīng)的對(duì)變量 W 的 read 或 write 動(dòng)作。如果 A 先于 B,那么 P 先于 Q。

上面三條規(guī)則有點(diǎn)復(fù)雜,我們來(lái)一條條講解下。

首先,我們來(lái)看看第一條規(guī)則。

只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的前一個(gè)動(dòng)作是 load 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 use 動(dòng)作。

load 動(dòng)作,指的是把從主內(nèi)存得到的變量值,放入到工作內(nèi)存的變量副本。use 動(dòng)作,指的是將工作內(nèi)存的一個(gè)變量值,傳遞給執(zhí)行引擎。那么這句話合起來(lái)的意思可以理解為:要使用變量 V 之前,必須去主內(nèi)存讀取變量 V。

并且,只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的后一個(gè)動(dòng)作是 use 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 load 動(dòng)作。

這句的意思可以理解為:要去讀取主內(nèi)存的變量值放入工作內(nèi)存的變量副本,那就必須使用它。

總的來(lái)說(shuō),這條規(guī)則的意思是:線程對(duì)變量 V 的 use 動(dòng)作,必須與 read、load 動(dòng)作連在一起,即 read -> load -> use 必須一起出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次使用 V 前都必須先從主內(nèi)存刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)變量 V 所做的修改后的值。

我們繼續(xù)看第二條規(guī)則。

只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的前一個(gè)動(dòng)作是 assign 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 store 動(dòng)作。

assign 動(dòng)作,指的是將執(zhí)行引擎的值賦值給工作內(nèi)存的變量。store 動(dòng)作,指的是將工作內(nèi)存的一個(gè)變量傳送到主內(nèi)存,方便后續(xù)寫(xiě)回主內(nèi)存。那么這句話合起來(lái)的意思可以理解為:要講工作內(nèi)存的變量寫(xiě)回主內(nèi)存,那么必須是工作內(nèi)存的變量收到執(zhí)行引擎的賦值。

并且,只有當(dāng)線程 T 對(duì)變量 V 執(zhí)行的后一個(gè)動(dòng)作是 store 的時(shí)候,線程 T 才能對(duì)變量 V 執(zhí)行 assign 動(dòng)作。

這句話的意思可以理解為:要將執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,就必須把工作內(nèi)存變量的值寫(xiě)回主內(nèi)存。

總的來(lái)說(shuō),這條規(guī)則的意思是:線程對(duì)變量 V 的 assign 動(dòng)作,必須與 store、write 連在一起,即:assign -> store -> write 必須一起出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次修改 V 后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量 V 所做的修改。

我們繼續(xù)看第三條規(guī)則。

假定動(dòng)作 A 是線程 T 對(duì)變量 V 實(shí)施的 use 或 assign 動(dòng)作,假定動(dòng)作 F 是和動(dòng)作 A 相關(guān)聯(lián)的 load 或 store 動(dòng)作,假定動(dòng)作 P 是和動(dòng)作 F 相應(yīng)的對(duì)變量 V 的 read 或 write 動(dòng)作。

這句話意思比較簡(jiǎn)單,use 和 assign 動(dòng)作分別是從工作內(nèi)存?zhèn)鬟f變量給執(zhí)行引擎,以及從執(zhí)行引擎?zhèn)鬟f變量給工作內(nèi)存。load 和 store 動(dòng)作分別是從主內(nèi)存載入數(shù)據(jù)到工作內(nèi)存,以及從工作內(nèi)存寫(xiě)數(shù)據(jù)到主內(nèi)存。read 和 write 動(dòng)作分別是將數(shù)據(jù)讀取到工作內(nèi)存,以及將數(shù)據(jù)寫(xiě)回主內(nèi)存。

我們假設(shè)是一個(gè)寫(xiě)入到主內(nèi)存動(dòng)作,如果這幾個(gè)組合起來(lái),那么就是:A -> F -> P(assign -> store -> write)。

類(lèi)似的,假定動(dòng)作 B 是線程 T 對(duì)變量 W 實(shí)施的 use 或 assign 動(dòng)作,假定動(dòng)作 G 是和動(dòng)作 B 相關(guān)聯(lián)的 load 或 store 動(dòng)作,假定動(dòng)作 Q 是和動(dòng)作 G 相應(yīng)的對(duì)變量 W 的 read 或 write 動(dòng)作。

與上面類(lèi)似,如果是一個(gè)寫(xiě)入到主內(nèi)存動(dòng)作,如果這幾個(gè)組合起來(lái),那么就是:B -> G -> Q(assign -> store -> write)。

如果 A 先于 B,那么 P 先于 Q。

這個(gè)的意思是,如果 A 動(dòng)作早于 B 動(dòng)作發(fā)生,那么 A 動(dòng)作對(duì)應(yīng)的 P 動(dòng)作(write 動(dòng)作)就要早于 Q 動(dòng)作 (write 動(dòng)作)。

這條規(guī)則要求 volatile 修飾的變量不會(huì)被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同。

所以說(shuō) volatile 變量的可見(jiàn)性以及禁止重排序的語(yǔ)義,其實(shí)都來(lái)源于 Java 內(nèi)存模型里對(duì)于 volatile 變量的定義。

總結(jié)

這篇文章,我們介紹了 volatile 的兩個(gè)語(yǔ)義:

  • 可見(jiàn)性
  • 禁止重排序

可見(jiàn)性指的是 volatile 類(lèi)型的變量,其變量值一旦被修改,其他線程就能夠立刻感知到。而禁止重排序指的是被 volatile 修飾的變量,其執(zhí)行順序不能被重排序。我們?cè)谌粘J褂弥?,如果要?volatile 變量不發(fā)生線程安全問(wèn)題,只需要遵守下面兩個(gè)規(guī)則即可。

  • 運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
  • 變量不需要與其他的狀態(tài)變量共同參與不變約束。

最后,我們進(jìn)一步探究了 volatile 可見(jiàn)性以及禁止重排序的來(lái)源,其實(shí)就是 Java 內(nèi)存模型里對(duì)于 volatile 變量的定義。

責(zé)任編輯:武曉燕 來(lái)源: 陳樹(shù)義
相關(guān)推薦

2019-09-04 14:14:52

Java編程數(shù)據(jù)

2020-11-11 08:45:48

Java

2023-10-04 00:04:00

C++extern

2023-09-24 13:58:20

C++1auto

2024-02-26 10:36:59

C++開(kāi)發(fā)關(guān)鍵字

2011-06-14 13:26:27

volatile

2011-06-21 09:50:51

volatile

2012-03-01 12:50:03

Java

2022-08-17 07:53:10

Volatile關(guān)鍵字原子性

2009-06-29 18:14:23

Java多線程volatile關(guān)鍵字

2023-06-26 08:02:34

JSR重排序volatile

2011-07-14 23:14:42

C++static

2020-07-17 20:15:03

架構(gòu)JMMvolatile

2024-03-15 08:18:25

volatileAtomic關(guān)鍵字

2023-11-20 22:19:10

C++static

2018-01-19 10:43:06

Java面試官volatile關(guān)鍵字

2022-02-08 08:31:52

const關(guān)鍵字C語(yǔ)言

2024-11-20 15:55:57

線程Java開(kāi)發(fā)

2021-08-15 08:11:54

AndroidSynchronize關(guān)鍵字

2010-06-01 15:25:27

JavaCLASSPATH
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)