下文筆者將詳細介紹volatile這一篇文章,將使你真真的了解到volatile關鍵字的用法,如下所示:
volatile關鍵字 的功能:
我們都知道volatile關鍵字有兩個功能:
1.保證變數的記憶體可見性
2.禁止指令重排序
可見性,例:
/** * 變數的記憶體可見性例子 * * @author java265.com */ public class VolatileExample { /** * main 方法作為一個主執行緒 */ public static void main(String[] args) { MyThread myThread = new MyThread(); // 開啟執行緒 myThread.start(); // 主執行緒執行 for (; ; ) { if (myThread.isFlag()) { System.out.println("主執行緒訪問到 flag 變數"); } } } } /** * 子執行緒類 */ class MyThread extends Thread { private boolean flag = false; @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 修改變數值 flag = true; System.out.println("flag = " + flag); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
以上的代碼運行,控制臺永遠都不會列印出 “主執行緒訪問到 flag 變數”
那到底為什么呢?明明將flag設定為true,但是主執行緒無法讀取到最新的flag值,所以無法輸出“主執行緒訪問到 flag 變數”這句話,這就是變數可見性的一種示例,此時我們應該思考為什么會出現這種現象呢?這應該從jvm運行程式的記憶體模型
Java 記憶體模型
JMM(Java Memory Model):Java 記憶體模型,是 Java 虛擬機規范中所定義的一種記憶體模型,Java 記憶體模型是標準化的,屏蔽掉了底層不同計算機的區別,也就是說,JMM 是 JVM 中定義的一種并發編程的底層模型機制,
JMM 定義了執行緒和主記憶體之間的抽象關系:執行緒之間的共享變數存盤在主記憶體中,每個執行緒都有一個私有的本地記憶體,本地記憶體中存盤了該執行緒以讀/寫共享變數的副本,
JMM 的規定:
- 所有的共享變數都存盤于主記憶體,這里所說的變數指的是實體變數和類變數,不包含區域變數,因為區域變數是執行緒私有的,因此不存在競爭問題,
每一個執行緒還存在自己的作業記憶體,執行緒的作業記憶體,保留了被執行緒使用的變數的作業副本,
執行緒對變數的所有的操作(讀,取)都必須在作業記憶體中完成,而不能直接讀寫主記憶體中的變數,
不同執行緒之間也不能直接訪問對方作業記憶體中的變數,執行緒間變數的值的傳遞需要通過主記憶體中轉來完成,
JMM 的抽象示意圖:

JMM 這樣的設定會導致執行緒對共享變數的修改沒有即時更新到主記憶體,或執行緒沒能夠即時將共享變數的最新值同步到作業記憶體中,從而使得執行緒在使用共享變數的值時,該值并不是最新的
那么如何解決這個問題呢?
加鎖 和 使用 volatile 關鍵字
使用 synchronizer 進行加鎖
/** * java265.com 示例程式 * main 方法作為一個主執行緒 */ public static void main(String[] args) { MyThread myThread = new MyThread(); // 開啟執行緒 myThread.start(); // 主執行緒執行 for (; ; ) { synchronized (myThread) { if (myThread.isFlag()) { System.out.println("主執行緒訪問到 flag 變數"); } } } }
當一個執行緒進入 synchronizer 代碼塊后
執行緒獲取到鎖,會清空本地記憶體,然后從主記憶體中拷貝共享變數的最新值到本地記憶體作為副本,代碼運行完畢后,又將修改后的副本值重繪到主記憶體中,最后執行緒釋放鎖
使用 volatile 關鍵字
/** * 子執行緒類 */ class MyThread extends Thread { private volatile boolean flag = false; @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 修改變數值 flag = true; System.out.println("flag = " + flag); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
當一個變數被 volatile 修飾后,每個執行緒要操作變數時會從主記憶體中將變數拷貝到本地記憶體作為副本,當執行緒操作變數副本并寫回主記憶體后,會通過 CPU 總線嗅探機制告知其他執行緒該變數副本已經失效,需要重新從主記憶體中讀取,
volatile關鍵字也保證不了共享變數的可見性,只是采用修改變數后都寫回到主記憶體中,然后通過其他機制告知變數副本已失效,讓其他執行緒使用時重新獲取
volatile功能2:禁止指令重排序
為了提高性能,在遵守 as-if-serial 語意(即不管怎么重排序,單執行緒下程式的執行結果不能被改變,編譯器,runtime 和處理器都必須遵守,)的情況下,編譯器和處理器常常會對指令做重排序,
一般重排序可以分為如下三種型別:
編譯器優化重排序,編譯器在不改變單執行緒程式語意的前提下,可以重新安排陳述句的執行順序,
指令級并行重排序,現代處理器采用了指令級并行技術來將多條指令重疊執行,如果不存在資料依賴性,處理器可以改變陳述句對應機器指令的執行順序,
記憶體系統重排序,由于處理器使用快取和讀 / 寫緩沖區,這使得加載和存盤操作看上去可能是在亂序執行,
資料依賴性:如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性,這里所說的資料依賴性僅針對單個處理器中執行的指令序列和單個執行緒中執行的操作,不同處理器之間和不同執行緒之間的資料依賴性不被編譯器和處理器考慮,
從 Java 源代碼到最終執行的指令序列,會分別經歷下面三種重排序

int a = 0; // 執行緒 A a = 1; // 1 flag = true; // 2 // 執行緒 B if (flag) { // 3 int i = a; // 4 }
從上面的代碼好像沒有問題,最后 i 的值是 1
但是為了提高性能,編譯器和處理器常常會在不改變資料依賴的情況下對指令做重排序
假設執行緒 A 在執行時被重排序成先執行代碼 2,再執行代碼 1;而執行緒 B 在執行緒 A 執行完代碼 2 后,讀取了 flag 變數,由于條件判斷為真,執行緒 B 將讀取變數 a,此時,變數 a 還根本沒有被執行緒 A 寫入,那么 i 最后的值是 0,導致執行結果不正確,為了保證代碼運行的順序性質,此時就需使用volatile關鍵字
上例中, 使用 volatile 不僅保證了變數的記憶體可見性,還禁止了指令的重排序,即保證了 volatile 修飾的變數編譯后的順序與程式的執行順序一樣,那么使用 volatile 修飾 flag 變數后,在執行緒 A 中,保證了代碼 1 的執行順序一定在代碼 2 之前,
volatile 在單例模式中的應用:
我們都知道單例的實作模式有很多種,下文筆者講述volatile在單例模式中的應用,例:
多執行緒下,此單例模式,永遠都只回傳一個單例的Singleton
public class Singleton { // volatile 保證可見性和禁止指令重排序 private static volatile Singleton singleton; public static Singleton getInstance() { // 第一次檢查 if (singleton == null) { // 同步代碼塊 synchronized(this.getClass()) { // 第二次檢查 if (singleton == null) { // 物件的實體化是一個非原子性操作 singleton = new Singleton(); } } } return singleton; } }
從以上的代碼中,我們可以看出new Singleton() 是一個非原子性操作
物件實體化分為三步操作:(1)分配記憶體空間,(2)初始化實體,(3)回傳記憶體地址給參考,所以,在使用構造器創建物件時,編譯器可能會進行指令重排序,假設執行緒 A 在執行創建物件時,(2)和(3)進行了重排序,如果執行緒 B 在執行緒 A 執行(3)時拿到了參考地址,并在第一個檢查中判斷 singleton != null 了,但此時執行緒 B 拿到的不是一個完整的物件,在使用物件進行操作時就會出現問題,
所以,這里使用 volatile 修飾 singleton 變數,就是為了禁止在實體化物件時進行指令重排序
從以上的分析,我們可以得出volatile關鍵字的相關說明:
volatile 修飾符適用于以下場景:某個屬性被多個執行緒共享,其中有一個執行緒修改了此屬性,其他執行緒可以立即得到修改后的值;或者作為狀態變數,如 flag = ture,實作輕量級同步,
volatile 屬性的讀寫操作都是無鎖的,它不能替代 synchronized,因為它沒有提供原子性和互斥性,因為無鎖,不需要花費時間在獲取鎖和釋放鎖上,所以說它是低成本的,
volatile 只能作用于屬性,我們用 volatile 修飾屬性,這樣編譯器就不會對這個屬性做指令重排序,
volatile 提供了可見性,任何一個執行緒對其的修改將立馬對其他執行緒可見,
volatile 提供了 happens-before 保證,對 volatile 變數 V 的寫入 happens-before 所有其他執行緒后續對 V 的讀操作,
volatile 可以使純賦值操作是原子的,如 boolean flag = true; falg = false,
volatile 可以在單例雙重檢查中實作可見性和禁止指令重排序,從而保證安全性,
參考資料:
http://www.java265.com/JavaCourse/202111/1746.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/484537.html
標籤:Java
