面試被問傻了,同事說不懂volatile關鍵字,由淺入深講解volatile

面試被問傻了,同事說不懂volatile關鍵字,由淺入深講解volatile

Java中Volatile關鍵字詳解

前言

隨着互聯網企業的興起,對我們技術的要求也越來越高,很多時候企業又想省錢,又想發揮出機器的最大性能,真是累壞了程序員們。

當然,想要適應社會的進步,程序員也要不斷的給自己充電,但人能忘本,基礎知識還是要學紮實的。

這不,有位同學就來找我訴苦了,前兩次面試都挺順利的,到了三面竟然栽在了volatile關鍵字上。

 

下面我們就來好好聊聊volatile

volatile

volatile 是一個類型修飾符。volatile 的作用是作爲指令關鍵字,確保本條指令不會因編譯器的優化而省略

volatile 的特性

保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)禁止進行指令重排序。(實現有序性)volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。關於volatile 原子性可以理解爲把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步,就跟下面的SoWhat跟SynSoWhat功能類似哦。

class SoWhat{volatile int i = 0; // volatile修飾的變量public int getI(){return i;// 單個volatile變量的讀}public void setI(int j){this.i = j; // 單個volatile 變量的寫}public void inc(){i++;//複合多個volatile 變量}}class SynSoWhat{int i = 0;public synchronized int getI(){return i;}public synchronized void setI(int j){this.i = j;}public void inc(){ // 普通方法調用int tmp = getI(); // 調用已同步方法tmp = tmp + 1;//普通寫方法setI(tmp);// 調用已同步方法}}

寫理解

volatile寫的內存語義如下:

當寫一個volatile變量時,JMM會把該線程對應的本地中的共享變量值刷新到主內存。

public class VolaSemanteme {int a = 0; volatile boolean flag = false; // 這是重點哦 public void init() { a = 1; flag = true; //....... } public void use() { if (flag) { int i = a * a; } //....... }}

線程A調用init方法,線程B調用use方法。

 

讀理解

volatile讀的內存語義如下:

當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

public class VolaSemanteme {int a = 0; volatile boolean flag = false; // 這是重點哦 public void init() { a = 1; flag = true; //....... } public void use() { if (flag) { int i = a * a; } //....... }}

流程圖大致是這樣的:

 

volatile 指令重排

volatile 變量的內存可見性是基於內存屏障(Memory Barrier)實現。關於內存屏障的具體講解以前寫過不再重複,JMM裝逼於無形這裏說過。總結來說就是JMM內部會有指令重排,並且會有af-if-serial跟happen-before的理念來保證指令的正確性。內存屏障就是基於4個彙編級別的關鍵字來禁止指令重排的,其中volatile的規則如下:

第一個爲讀操作時,第二個任何操作不可重排序到第一個前面。第二個爲寫操作時,第一個任何操作不可重排序到第二個後面。第一個爲寫操作時,第二個的讀寫操作也不運行重排序。

 

volatile寫底層實現

JMM對volatile的內存屏障插入策略

在每個volatile寫操作的前面插入一個StoreStore屏障。在每個volatile寫操作的後面插入一個StoreLoad屏障。

 

volatile 讀底層

JMM對volatile的內存屏障插入策略

在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個LoadStore屏障。

 

中重點說下volatile讀後面爲什麼跟了個LoadLoad。加入我有如下代碼 AB兩個線程執行,B線程的flag獲取下面的讀被提前了。

 

 

volatile的實現原理

有volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令。在CPU級別的功能如下:

將當前處理器緩存行的數據寫回到系統內存

這個寫回內存的操作會告知在其他CPU你們拿到的變量是無效的下一次使用時候要重新共享內存拿。

我們可以通過jitwatch對簡單的代碼進行詳細的反彙編看一下。

package com.sowhat.demo;public class VolaSemanteme {int unvloatileVal = 0;volatile boolean flag = false;public void init() {unvloatileVal = 1;flag = true; // 第九行哦}public void use() {if (flag) {int LocalA = unvloatileVal;if (LocalA == 0) {throw new RuntimeException("error");}}}public static void main(String[] args) {VolaSemanteme volaSemanteme = new VolaSemanteme();volaSemanteme.init();volaSemanteme.use();}}

對普通變量的賦值操作:

 

對volatile變量的賦值操作。

 

可以對比得出,volatile 修飾的變量確實會多一個 lock addl $0x0,(%rsp) 指令。

0x0000000114ce95cb: lock addl $0x0,(%rsp) ;*putfield flag; - com.sowhat.demo.VolaSemanteme::init@7 (line 9)

使用 volatile 必須具備的條件

對變量的寫操作不依賴於當前值。該變量沒有包含在具有其他變量的不變式中。只有在狀態真正獨立於程序內其他內容時才能使用 volatile。

狀態標誌

也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;......public void shutdown() { shutdownRequested = true; }public void doWork() {while (!shutdownRequested) {// do stuff}}

一次性安全發佈(one-time safe publication)

缺乏同步會導致無法實現可見性,這使得確定何時寫入對象引用而不是原始值變得更加困難。在缺乏同步的情況下,可能會遇到某個對象引用的更新值(由另一個線程寫入)和該對象狀態的舊值同時存在。(這就是造成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的情況下進行讀操作,產生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的對象)。

public class BackgroundFloobleLoader {public volatile Flooble theFlooble;public void initInBackground() {// do lots of stufftheFlooble = new Flooble(); // this is the only write to theFlooble}}public class SomeOtherClass {public void doWork() {while (true) {// do some stuff...// use the Flooble, but only if it is readyif (floobleLoader.theFlooble != null)doSomething(floobleLoader.theFlooble);}}}

模獨立觀察(independent observation)

安全使用 volatile 的另一種簡單模式是定期 發佈 觀察結果供程序內部使用。例如,假設有一種環境傳感器能夠感覺環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。然後,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值。

public class UserManager {public volatile String lastUser;public boolean authenticate(String user, String password) {boolean valid = passwordIsValid(user, password);if (valid) {User u = new User();activeUsers.add(u);lastUser = user;}return valid;}}

volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯。此外,對於對象引用的數據成員,引用的對象必須是有效不可變的。(這將禁止具有數組值的屬性,因爲當數組引用被聲明爲 volatile 時,只有引用而不是數組本身具有 volatile 語義)。對於任何 volatile 變量,不變式或約束都不能包含 JavaBean 屬性。

@ThreadSafepublic class Person {private volatile String firstName;private volatile String lastName;private volatile int age;public String getFirstName() { return firstName; }public String getLastName() { return lastName; }public int getAge() { return age; }public void setFirstName(String firstName) {this.firstName = firstName;}public void setLastName(String lastName) {this.lastName = lastName;}public void setAge(int age) {this.age = age;}}

開銷較低的讀-寫鎖策略

volatile 的功能還不足以實現計數器。因爲 ++x 實際上是三種操作(讀、添加、存儲)的簡單組合,如果多個線程湊巧試圖同時對 volatile 計數器執行增量操作,那麼它的更新值有可能會丟失。 如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。 安全的計數器使用 synchronized 確保增量操作是原子的,並使用 volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因爲讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優於一個無競爭的鎖獲取的開銷。

@ThreadSafepublic class CheesyCounter {// Employs the cheap read-write lock trick// All mutative operations MUST be done with the 'this' lock held@GuardedBy("this") private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value++;}}

雙重檢查(double-checked)

單例模式的一種實現方式,但很多人會忽略 volatile 關鍵字,因爲沒有該關鍵字,程序也可以很好的運行,只不過代碼的穩定性總不是 100%,說不定在未來的某個時刻,隱藏的 bug 就出來了。

class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {syschronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}}

推薦懶加載優雅寫法 Initialization on Demand Holder(IODH)。public class Singleton {static class SingletonHolder {static Singleton instance = new Singleton();}public static Singleton getInstance(){return SingletonHolder.instance;}}

好了,今天就聊到這裏,謝謝大家的閱讀,喜歡可以關注我哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章