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