java多线程中volatile关键字
一:計(jì)算機(jī)中的內(nèi)存模型
計(jì)算機(jī)中指令都通過CPU去執(zhí)行,執(zhí)行執(zhí)行的時(shí)候一般都會(huì)涉及到讀寫,我們都知道CUP的計(jì)算速度是很快的,如果都把數(shù)據(jù)放到我們的主存中則會(huì)造成CPU每執(zhí)行一條指令都要等待的問題,這個(gè)時(shí)候高速緩存Cache應(yīng)運(yùn)而生。Cache就是把一些處理的中間數(shù)據(jù)緩存起來大大加快了指令的處理速度。
以上的模型針對(duì)于單核CPU是沒有問題的,但是多核CPU的話就會(huì)產(chǎn)生數(shù)據(jù)不一致的情況。真實(shí)的計(jì)算機(jī)內(nèi)存模型如下。
這個(gè)內(nèi)存模型比我們想的多了個(gè)“緩存一致性協(xié)議或者總線鎖機(jī)制”,這個(gè)東東就是解決我們上面說的緩存不一致的問題。
為了解決緩存一致性的問題,現(xiàn)代計(jì)算機(jī)系統(tǒng)需要各個(gè)處理器讀寫緩存時(shí)遵循一些協(xié)議(MSI、MESI、MOSI、Synapse、Firefly、DragonProtocal,這些都是緩存協(xié)議),按照協(xié)議來進(jìn)行讀寫訪問緩存。
其實(shí),除了緩存之外,處理器還會(huì)對(duì)輸入的代碼程序在保證結(jié)果不變的情況下進(jìn)行重排序,這就是著名的“指令重排序”,旨在提高運(yùn)行效率。
例如:
這里a=0,b=1兩句可以隨便排序,不影響程序邏輯結(jié)果,但c=a+b這句必須在前兩句的后面執(zhí)行。
二:內(nèi)存模型中三個(gè)概念
1. 原子性(Atomicity)
原子性指的是操作不可中斷,不可分割的原子操作。Java內(nèi)存模型直接用來保證原子性變量的操作包括use、read、load、assign、store、write,我們大致可以認(rèn)為Java基本數(shù)據(jù)類型的訪問都是原子性的,如果用戶要操作一個(gè)更大的范圍保證原子性,Java內(nèi)存模型還提供了lock和unlock來滿足這種需求,但是這兩種操作沒有直接開放給用戶,而是提供了兩個(gè)更高層次的字節(jié)碼指令:monitorenter 和 moniterexit,這兩個(gè)指令對(duì)應(yīng)到Java代碼中就是synchronized關(guān)鍵字,所以synchronized代碼塊之間的操作具有原子性。
2. 可見性(Visibility)
可見性指的一個(gè)變量的是共享的,一個(gè)線程修改,其他線程立刻可見。在Java中,除了volatile可以實(shí)現(xiàn)可見性之外,synchronized和final關(guān)鍵字也能實(shí)現(xiàn)可見性。synchronized同步塊的可見性是因?yàn)閷?duì)一個(gè)變量執(zhí)行unlock操作之前,必須將變量的改動(dòng)寫回主內(nèi)存來(store、write兩個(gè)操作)實(shí)現(xiàn)的。而final字段則是因?yàn)橐坏ゝinal字段初始化完成,其他線程就可以訪問final字段的值,而且final字段初始化完成之后就不再可變。
3. 有序性(Ordering)
處理器會(huì)對(duì)指令或者程序進(jìn)行重排序優(yōu)化。這種優(yōu)化在單線程處理中不會(huì)存在問題,但是多線程條件下可能會(huì)出問題。Java中提供了volatile和synchronized關(guān)鍵字來保證線程間操作的有序性。
三:Java內(nèi)存模型
1. Java主內(nèi)存和工作內(nèi)存
Java內(nèi)訓(xùn)模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存中,對(duì)應(yīng)就是Java的堆內(nèi)存。每個(gè)線程都有自己獨(dú)有的工作內(nèi)存,工作內(nèi)存中變量來自主內(nèi)存副本拷貝,線程對(duì)變量的讀寫操作都必須在工作內(nèi)存中機(jī)型,不能直接讀寫主存中的變量。工作內(nèi)存也是相互獨(dú)立的。交互圖如下:
由圖可知如果兩個(gè)線程同時(shí)操作同一個(gè)共享變量,則可能產(chǎn)生數(shù)據(jù)不一致的問題。
2. 內(nèi)存交互操作
Java內(nèi)存模型為主內(nèi)存和工作內(nèi)存間的變量拷貝及同步寫回定義了具體的實(shí)現(xiàn)協(xié)議,該協(xié)議主要由8種操作來完成。
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀取):作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
- load(載入):作用于工作內(nèi)存的變量,它把通過read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把通過store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
線程、工作內(nèi)存、主內(nèi)存對(duì)應(yīng)這8種操作的交互關(guān)系圖如下:
根據(jù)交互圖,我們可以看出,read和load要順序執(zhí)行,如果把變量從工作內(nèi)存同步回主存需要先執(zhí)行store和write操作。除此之外,Java內(nèi)存模型對(duì)這8中操作還存在著其他的約束:
- 只允許read和load、store和write這兩對(duì)操作成對(duì)出現(xiàn)。
- 不允許線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變之后,必須同步回寫到主內(nèi)存。
- 不允許線程把沒有經(jīng)過assign操作的變量,同步回寫到主內(nèi)存。
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中使用未經(jīng)初始化的變量,即對(duì)一個(gè)變量進(jìn)行use、store操作之前,必須先執(zhí)行過load、assign操作。
- 一個(gè)變量在同一時(shí)刻只能被一條線程執(zhí)行l(wèi)ock操作,一旦lock成功,可以被同一線程重復(fù)lock多次,多次執(zhí)行l(wèi)ock之后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。
- 對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中該變量的值,所以在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作對(duì)其進(jìn)行初始化。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把該變量同步回主內(nèi)存(執(zhí)行store、write操作)。
- 如果一個(gè)變量事先沒有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許unlock一個(gè)被其他線程lock的變量。
四:volatile關(guān)鍵字
volatile滿足兩層含義:可見性、有序性。舉例說明此關(guān)鍵字使用場景。
場景一:使用volatile修飾的變量做主線程和子線程之間的通信。
public class VolatileTest {//不使用volatile關(guān)鍵字,線程會(huì)一直死循環(huán) // private static Boolean stop = false;//使用volatile關(guān)鍵字private static volatile Boolean stop = false;public static void main(String args[]) throws InterruptedException {//新建立一個(gè)線程Thread workThread = new Thread() {@Overridepublic void run() {getThreadLog("線程開始執(zhí)行!");while (true) {if (stop) {break;}}getThreadLog("線程執(zhí)行結(jié)束了!");}};//啟動(dòng)該線程workThread.start();//休眠一會(huì)兒,讓子線程飛一會(huì)兒Thread.sleep(1000);//主線程將stop置為truestop = true;//打印日志getThreadLog("主線程執(zhí)行結(jié)束了");//使用join方法繼續(xù)執(zhí)行子線程workThread.join();}/*** 獲取線程名和時(shí)間** @return*/public static void getThreadLog(String logContent) {StringBuffer stringBuffer = new StringBuffer();stringBuffer.append("[");stringBuffer.append(Thread.currentThread().getName());stringBuffer.append(" ");stringBuffer.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));stringBuffer.append("]");stringBuffer.append(logContent);System.out.println(stringBuffer.toString());}場景二:使用volatile修飾在單例模式中體現(xiàn)
public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class){if(uniqueInstance == null){//進(jìn)入?yún)^(qū)域后,再檢查一次,如果仍是null,才創(chuàng)建實(shí)例uniqueInstance = new Singleton();}}}return uniqueInstance;} }注意: volatile不滿足原子性,因此使用此關(guān)鍵字進(jìn)行多線程修改共享變量會(huì)出問題。
五:內(nèi)存屏障
為什么會(huì)有內(nèi)存屏障?
作用
Load Barrier 讀屏障
在指令前插入Load Barrier,可以讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制重新從主內(nèi)存加載數(shù)據(jù);
Store Barrier 寫屏障
利用緩存一致性機(jī)制強(qiáng)制將對(duì)緩存的修改操作立即寫入主存,讓其他線程可見,并且緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上CPU緩存的內(nèi)存區(qū)域數(shù)據(jù)。
內(nèi)存屏障類型
為了保證可見性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。
其中StoreLoad指令是現(xiàn)代多處理器都需要使用的,但是它的開銷也很昂貴。
volatile插入屏障策略
實(shí)現(xiàn)原理
volatile變量 寫匯編指令會(huì)多出#Lock前綴,Lock前綴在多核處理器下的作用:
參考:
JMM和底層實(shí)現(xiàn)原理(https://www.jianshu.com/p/8a58d8335270)
總結(jié)
以上是生活随笔為你收集整理的java多线程中volatile关键字的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: An internal error oc
- 下一篇: J.U.C系列(五)BlockingQu