前言
volatile相關的知識點,在面試程序中,屬于基礎問題,是必須要掌握的知識點,如果回答不上來會嚴重扣分的哦,
volatile關鍵字基本介紹
volatile可以看成是synchronized的一種輕量級的實作,但volatile并不能完全代替synchronized,volatile有synchronized可見性的特性,但沒有synchronized原子性的特性,
可見性即用volatile關鍵字修飾的成員變數表明該變數不存在作業執行緒的副本,執行緒每次直接都從主記憶體中讀取,每次讀取的都是最新的值,這也就保證了變數對其他執行緒的可見性,
另外,使用volatile還能確保變數不能被重排序,保證了有序性,
-
當一個變數定義為volatile之后,它將具備兩種特性:
- 保證此變數對所有執行緒的可見性
- 禁止指令重排序優化
-
volatile與synchronized的區別:
- 1、volatile只能修飾實體變數和類變數,而synchronized可以修飾方法,以及代碼塊,
- 2、volatile保證資料的可見性,但是不保證原子性; 而synchronized是一種排他(互斥)的機制,既保證可見性,又保證原子性,
- 3、volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞,
- 4、volatile可以看做是輕量版的synchronized,volatile不保證原子性,但是如果是對一個共享變數進行多個執行緒的賦值,而沒有其他的操作,那么就可以用volatile來代替synchronized,因為賦值本身是有原子性的,而volatile又保證了可見性,所以就可以保證執行緒安全了,
保證此變數對所有執行緒的可見性:
當一條執行緒修改了這個變數的值,新值對于其他執行緒可以說是可以立即得知的,Java記憶體模型規定了所有的變數都存盤在主記憶體,每條執行緒還有自己的作業記憶體,執行緒的作業記憶體保存了該執行緒使用到的變數在主記憶體的副本拷貝,執行緒對變數的所有操作都必須在作業記憶體中進行,而不能直接讀取主記憶體中的變數,
知識拓展:記憶體可見性:
- 概念:JVM記憶體模型:主記憶體 和 執行緒獨立的 作業記憶體,Java記憶體模型規定,對于多個執行緒共享的變數,存盤在主記憶體當中,每個執行緒都有自己獨立的作業記憶體(比如CPU的暫存器),執行緒只能訪問自己的作業記憶體,不可以訪問其它執行緒的作業記憶體,作業記憶體中保存了主記憶體共享變數的副本,執行緒要操作這些共享變數,只能通過操作作業記憶體中的副本來實作,操作完畢之后再同步回到主記憶體當中,
- 如何保證多個執行緒操作主記憶體的資料完整性是一個難題,Java記憶體模型也規定了作業記憶體與主記憶體之間互動的協議,定義了8種原子操作:
- lock:將主記憶體中的變數鎖定,為一個執行緒所獨占,
- unclock:將lock加的鎖定解除,此時其它的執行緒可以有機會訪問此變數,
- read:將主記憶體中的變數值讀到作業記憶體當中,
- load:將read讀取的值保存到作業記憶體中的變數副本中,
- use:將值傳遞給執行緒的代碼執行引擎,
- assign:將執行引擎處理回傳的值重新賦值給變數副本,
- store:將變數副本的值存盤到主記憶體中,
- write:將store存盤的值寫入到主記憶體的共享變數當中,
通過上面Java記憶體模型的概述,我們會注意到這么一個問題,每個執行緒在獲取鎖之后會在自己的作業記憶體來操作共享變數,操作完成之后將作業記憶體中的副本回寫到主記憶體,并且在其它執行緒從主記憶體將變數同步回自己的作業記憶體之前,共享變數的改變對其是不可見的,
即其他執行緒的本地記憶體中的變數已經是過時的,并不是更新后的值,volatile保證可見性的原理是在每次訪問變數時都會進行一次重繪,因此每次訪問都是主記憶體中最新的版本,所以volatile關鍵字的作用之一就是保證變數修改的實時可見性,
即,volatile的特殊規則就是:
- read、load、use動作必須連續出現,
- assign、store、write動作必須連續出現,
所以,使用volatile變數能夠保證:
- 每次讀取前必須先從主記憶體重繪最新的值,
- 每次寫入后必須立即同步回主記憶體當中,
也就是說,volatile關鍵字修飾的變數看到的是自己的最新值,執行緒1中對變數v的最新修改,對執行緒2是可見的,
禁止指令重排序優化:
volatile boolean isOK = false; //假設以下代碼在執行緒A執行 A.init(); isOK=true; //假設以下代碼在執行緒B執行 while(!isOK){ sleep(); } B.init();
A執行緒在初始化的時候,B執行緒處于睡眠狀態,等待A執行緒完成初始化的時候才能夠進行自己的初始化,這里的先后關系依賴于isOK這個變數,
如果沒有volatile修飾isOK這個變數,那么isOK的賦值就可能出現在A.init()之前(指令重排序,Java虛擬機的一種優化措施),此時A沒有初始化,而B的初始化就破壞了它們之前形成的那種依賴關系,可能就會出錯,
知識拓展:指令重排序:
- 概念:指令重排序是JVM為了優化指令,提高程式運行效率,在不影響 單執行緒程式 執行結果的前提下,盡可能地提高并行度,編譯器、處理器也遵循這樣一個目標,注意是單執行緒,多執行緒的情況下指令重排序就會給程式帶來問題,
不同的指令間可能存在資料依賴,比如下面的陳述句:
int l = 3; // (1) int w = 4; // (2) int s = l * w; // (3)
面積的計算依賴于l與w兩個變數的賦值指令,而l與w無依賴關系,
重排序會遵守兩個規則:
- as-if-serial規則:as-if-serial規則是指不管如何重排序(編譯器與處理器為了提高并行度),(單執行緒)程式的結果不能被改變,這是編譯器、Runtime、處理器必須遵守的語意,
- happens-before規則:
- 程式順序規則:一個執行緒中的每個操作,happens-before于執行緒中的任意后續操作,
- 監視器鎖規則:一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖,
- volatile變數規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀,
- 傳遞性:如果(A)happens-before(B),且(B)happens-before(C),那么(A)happens-before(C),
- 執行緒start()規則:主執行緒A啟動執行緒B,執行緒B中可以看到主執行緒啟動B之前的操作,也就是start() happens-before 執行緒B中的操作,
- 執行緒join()規則:主執行緒A等待子執行緒B完成,當子執行緒B執行完畢后,主執行緒A可以看到執行緒B的所有操作,也就是說,子執行緒B中的任意操作,happens-before join()的回傳,
- 中斷規則:一個執行緒呼叫另一個執行緒的interrupt,happens-before于被中斷的執行緒發現中斷,
- 終結規則:一個物件的建構式的結束,happens-before于這個物件finalizer的開始,
- 概念:前一個操作的結果可以被后續的操作獲取,講直白點就是前面一個操作把變數a賦值為1,那后面一個操作肯定能知道a已經變成了1,
- happens-before(先行發生)規則如下:
雖然,(1)-happensbefore ->(2),(2)-happens before->(3),但是計算順序(1)(2)(3)與(2)(1)(3)對于l、w、area變數的結果并無區別,編譯器、Runtime在優化時可以根據情況重排序(1)與(2),而絲毫不影響程式的結果,
- volatile使用場景:
- 1、對變數的寫操作不依賴當前變數的值,
- 2、該變數沒有包含在其他變數的不變式中,
- 如果正確使用volatile的話,必須依賴下以下種條件:
也可以這樣理解,就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程式在并發時能夠正確執行,
第一個條件的限制使 volatile 變數不能用作執行緒安全計數器,雖然增量操作(i++)看上去類似一個單獨操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性,
實作正確的操作需要使 i 的值在操作期間保持不變,而 volatile 變數無法實作這點,
- 在以下兩種情況下都必須使用volatile:
- 1、狀態的改變,
- 2、讀多寫少的情況,
具體如下:
// 場景一:狀態改變 /** * 雙重檢查(DCL) */ public class Sun { private static volatile Sun sunInstance; private Sun() { } public static Sun getSunInstance() { if (sunInstance == null) { synchronized (Sun.class) { if (sunInstance == null){ sunInstance = new Sun(); } } } return sunInstance; } } // 場景二:讀多寫少 public class VolatileTest { private volatile int value; //讀操作,沒有synchronized,提高性能 public int getValue() { return value; } //寫操作,必須synchronized,因為x++不是原子操作 public synchronized int increment() { return value++; } }
問題來了,volatile是如何防止指令重排序優化的呢?
答:
volatile關鍵字通過 “記憶體屏障” 的方式來防止指令被重排序,為了實作volatile的記憶體語意,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序,大多數的處理器都支持記憶體屏障的指令,
對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎不可能,為此,Java記憶體模型采取保守策略,下面是基于保守策略的JMM記憶體屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障,
- 在每個volatile寫操作的后面插入一個StoreLoad屏障,
- 在每個volatile讀操作的后面插入一個LoadLoad屏障,
- 在每個volatile讀操作的后面插入一個LoadStore屏障,
知識拓展:記憶體屏障:
記憶體屏障(Memory Barrier,或有時叫做記憶體柵欄,Memory Fence)是一種CPU指令,用于控制特定條件下的重排序和記憶體可見性問題,Java編譯器也會根據記憶體屏障的規則禁止重排序,
記憶體屏障可以被分為以下幾種型別:
- LoadLoad屏障:對于這樣的陳述句Load1; LoadLoad; Load2,在Load2及后續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢,
- StoreStore屏障:對于這樣的陳述句Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見,
- LoadStore屏障:對于這樣的陳述句Load1; LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢,
- StoreLoad屏障:對于這樣的陳述句Store1; StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見,它的開銷是四種屏障中最大的,在大多數處理器的實作中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/69354.html
標籤:Java
上一篇:使用Intellij Idea開發,在servlet的doGet方法中使用JDBC連接資料庫總是報找不到資料庫驅動的錯誤,是為什么呢?
下一篇:面試題-JavaSE語法
