您好,登錄后才能下訂單哦!
java并發(fā)編程中怎么實現(xiàn)共享變量,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
可見性
如果一個線程對共享變量值的修改, 能夠及時的被其他線程看到, 叫做共享變量的可見性.
Java 虛擬機規(guī)范試圖定義一種 Java 內(nèi)存模型 (JMM), 來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異, 讓 Java 程序在各種平臺上都能達到一致的內(nèi)存訪問效果.
簡單來說, 由于 CPU 執(zhí)行指令的速度是很快的, 但是內(nèi)存訪問的速度就慢了很多, 相差的不是一個數(shù)量級, 所以搞處理器的那群大佬們又在 CPU 里加了好幾層高速緩存.
在 Java 內(nèi)存模型里, 對上述的優(yōu)化又進行了一波抽象. JMM 規(guī)定所有變量都是存在主存中的, 類似于上面提到的普通內(nèi)存, 每個線程又包含自己的工作內(nèi)存, 方便理解就可以看成 CPU 上的寄存器或者高速緩存.
所以線程的操作都是以工作內(nèi)存為主, 它們只能訪問自己的工作內(nèi)存, 且工作前后都要把值在同步回主內(nèi)存.
簡單點就是, 多線程中讀取或修改共享變量時, 首先會讀取這個變量到自己的工作內(nèi)存中成為一個副本, 對這個副本進行改動后, 再更新回主內(nèi)存中.
使用工作內(nèi)存和主存, 雖然加快的速度, 但是也帶來了一些問題. 比如看下面一個例子:
i = i + 1;
假設(shè) i
初值為 0
, 當只有一個線程執(zhí)行它時, 結(jié)果肯定得到 1
, 當兩個線程執(zhí)行時, 會得到結(jié)果 2
嗎? 這倒不一定了. 可能存在這種情況:
線程1: load i from 主存 // i = 0 i + 1 // i = 1線程2: load i from主存 // 因為線程1還沒將i的值寫回主內(nèi)存,所以i還是0 i + 1 //i = 1線程1: save i to 主存線程2: save i to 主存
如果兩個線程按照上面的執(zhí)行流程, 那么 i
最后的值居然是 1
了. 如果最后的寫回生效的慢, 你再讀取 i
的值, 都可能是 0
, 這就是緩存不一致問題.
這種情況一般稱為 失效數(shù)據(jù), 因為線程1 還沒將 i
的值寫回主內(nèi)存, 所以 i
還是 0
, 在線程2 中讀到的就是 i
的失效值(舊值).
也可以理解成, 在操作完成之后將工作內(nèi)存中的副本回寫到主內(nèi)存, 并且在其它線程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前, 共享變量的改變對其是不可見的.
有序性
有序性: 即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行. 舉個簡單的例子, 看下面這段代碼:
int i = 0; boolean flag = false;i = 1; //語句1 flag = true; //語句2
上面代碼定義了一個 int
型變量, 定義了一個 boolean
類型變量, 然后分別對兩個變量進行賦值操作.
從代碼順序上看, 語句1 是在語句2 前面的, 那么 JVM 在真正執(zhí)行這段代碼的時候會保證語句1 一定會在語句2 前面執(zhí)行嗎? 不一定, 為什么呢? 這里可能會發(fā)生指令重排序.
重排序
指令重排是指 JVM 在編譯 Java 代碼的時候, 或者 CPU 在執(zhí)行 JVM 字節(jié)碼的時候, 對現(xiàn)有的指令順序進行重新排序.
它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致, 但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的(指的是不改變單線程下的程序執(zhí)行結(jié)果).
雖然處理器會對指令進行重排序, 但是它會保證程序最終結(jié)果會和代碼順序執(zhí)行結(jié)果相同, 那么它靠什么保證的呢? 再看下面一個例子:
int a = 10; //語句1int r = 2; //語句2a = a + 3; //語句3r = a*a; //語句4
這段代碼有 4 個語句, 那么可能的一個執(zhí)行順序是:
那么可不可能是這個執(zhí)行順序呢?
語句2 語句1 語句4 語句3.
不可能, 因為處理器在進行重排序時是會考慮指令之間的數(shù)據(jù)依賴性, 如果一個指令 Instruction 2 必須用到 Instruction 1 的結(jié)果, 那么處理器會保證 Instruction 1 會在 Instruction 2 之前執(zhí)行.
雖然重排序不會影響單個線程內(nèi)程序執(zhí)行的結(jié)果, 但是多線程呢? 下面看一個例子:
//線程1:context = loadContext(); //語句1inited = true; //語句2 //線程2:while(!inited ){ sleep()}doSomethingwithconfig(context);
上面代碼中, 由于語句1 和語句2 沒有數(shù)據(jù)依賴性, 因此可能會被重排序.
假如發(fā)生了重排序, 在線程1 執(zhí)行過程中先執(zhí)行語句2, 而此時線程2 會以為初始化工作已經(jīng)完成, 那么就會跳出 while
循環(huán), 去執(zhí)行 doSomethingwithconfig(context)
方法, 而此時 context
并沒有被初始化, 就會導(dǎo)致程序出錯.
從上面可以看出, 指令重排序不會影響單個線程的執(zhí)行, 但是會影響到線程并發(fā)執(zhí)行的正確性.
原子性
Java 中, 對基本數(shù)據(jù)類型的讀取和賦值操作是原子性操作, 所謂原子性操作就是指這些操作是不可中斷的, 要做一定做完, 要么就沒有執(zhí)行.
JMM 只實現(xiàn)了基本的原子性, 像 i++
的操作, 必須借助于 synchronized
和 Lock
來保證整塊代碼的原子性了. 線程在釋放鎖之前, 必然會把 i
的值刷回到主存的.
重點, 要想并發(fā)程序正確地執(zhí)行, 必須要保證原子性、可見性以及有序性. 只要有一個沒有被保證, 就有可能會導(dǎo)致程序運行不正確.
volatile 關(guān)鍵字
volatile 關(guān)鍵字的兩層語義
一旦一個共享變量 (類的成員變量、類的靜態(tài)成員變量) 被 volatile
修飾之后, 那么就具備了兩層語義:
1) 禁止進行指令重排序.
2) 讀寫一個變量時, 都是直接操作主內(nèi)存.
在一個變量被 volatile
修飾后, JVM 會為我們做兩件事:
1.在每個 volatile
寫操作前插入 StoreStore
屏障, 在寫操作后插入 StoreLoad
屏障.
2.在每個 volatile
讀操作前插入 LoadLoad
屏障, 在讀操作后插入 LoadStore
屏障.
或許這樣說有些抽象, 我們看一看剛才線程A代碼的例子:
boolean contextReady = false;//在線程A中執(zhí)行:context = loadContext();contextReady = true;
我們給 contextReady
增加 volatile
修飾符, 會帶來什么效果呢?
由于加入了 StoreStore
屏障, 屏障上方的普通寫入語句 context = loadContext()
和屏障下方的 volatile
寫入語句 contextReady = true
無法交換順序, 從而成功阻止了指令重排序.
也就是說, 當程序執(zhí)行到 volatile
變量的讀或?qū)懖僮鲿r, 在其前面的操作的更改肯定全部已經(jīng)進行, 且結(jié)果已經(jīng)對后面的操作可見.
volatile特性之一:保證變量在線程之間的可見性. 可見性的保證是基于 CPU 的內(nèi)存屏障指令, 被 JSR-133 抽象為 happens-before
原則.
volatile特性之二:阻止編譯時和運行時的指令重排. 編譯時 JVM 編譯器遵循內(nèi)存屏障的約束, 運行時依靠 CPU 屏障指令來阻止重排.
volatile
除了保證可見性和有序性, 還解決了long
類型和double
類型數(shù)據(jù)的 8 字節(jié)賦值問題.虛擬機規(guī)范中允許對 64 位數(shù)據(jù)類型, 分為 2 次 32 位的操作來處理, 當讀取一個非volatile
類型的 long 變量時, 如果對該變量的讀操作和寫操作不在同一個線程中執(zhí)行, 那么很有可能會讀取到某個值得高 32 位和另一個值得低 32 位.
看完上述內(nèi)容,你們掌握java并發(fā)編程中怎么實現(xiàn)共享變量的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。