volatile 手摸手带你解析
點擊上方?好好學java?,選擇?星標?公眾號
重磅資訊、干貨,第一時間送達 今日推薦:終于放棄了單調的swagger-ui了,選擇了這款神器—knife4j個人原創100W+訪問量博客:點擊前往,查看更多前言
volatile 是 Java 里的一個重要的指令,它是由 Java 虛擬機里提供的一個輕量級的同步機制。一個共享變量聲明為 volatile 后,特別是在多線程操作時,正確使用 volatile 變量,就要掌握好其原理。
特性
volatile 具有可見性和有序性的特性,同時,對 volatile 修飾的變量進行單個讀寫操作是具有原子性。
這幾個特性到底是什么意思呢?
可見性:?當一個線程更新了 volatile 修飾的共享變量,那么任意其他線程都能知道這個變量最后修改的值。簡單的說,就是多線程運行時,一個線程修改 volatile 共享變量后,其他線程獲取值時,一定都是這個修改后的值。
有序性:?一個線程中的操作,相對于自身,都是有序的,Java 內存模型會限制編譯器重排序和處理器重排序。意思就會說 volatile 內存語義單個線程中是串行的語義。
原子性:?多線程操作中,非復合操作單個 volatile 的讀寫是具有原子性的。
可見性
可見性是在多線程中保證共享變量的數據有效,接下來我們通過有 volatile 修飾的變量和無 volatile 修飾的變量代碼的執行結果來做對比分析。
附上我歷時三個月總結的?Java面試思維導圖,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「思維導圖」即可獲取
無 volatile 修飾變量
以下是沒有 volatile 修飾變量代碼,通過創建兩個線程,來驗證 flag 被其中一個線程修改后的執行情況。
/** * Created by YANGTAO on 2020/3/15 0015. */public class ValatileDemo {static Boolean flag = true;public static void main(String[] args) {// A 線程,判斷其他線程修改 flag 之后,數據是否對本線程有效 new Thread(() -> { while (flag) {} System.out.printf("********** %s 線程執行結束!**********", Thread.currentThread().getName()); }, "A").start();// B 線程,修改 flag 值 new Thread(() -> { try { // 避免 B 線程比 A 線程先運行修改 flag 值 TimeUnit.SECONDS.sleep(1); flag = false; // 如果 flag 值修改后,讓 B 線程先打印信息 TimeUnit.SECONDS.sleep(2);System.out.printf("********** %s 線程執行結束!**********", Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } }, "B").start();}}上面代碼中,當 flag 初始值 true,被 B 線程修改為 false。如果修改后的值對 A 線程有效,那么正常情況下 A 線程會先于 B 線程結束。執行結果如下:
執行結果是:當 B 線程執行結束后, flag=false并未對 A 線程生效,A 線程死循環。
volatile 修飾變量
在上述代碼中,當我們把 flag 使用 volatile 修飾:
/** * Created by YANGTAO on 2020/3/15 0015. */public class ValatileDemo {static volatile Boolean flag = true;public static void main(String[] args) {// A 線程,判斷其他線程修改 flag 之后,數據是否對本線程有效 new Thread(() -> { while (flag) {} System.out.printf("********** %s 線程執行結束!**********", Thread.currentThread().getName()); }, "A").start();// B 線程,修改 flag 值 new Thread(() -> { try { // 避免 B 線程比 A 線程先運行修改 flag 值 TimeUnit.SECONDS.sleep(1); flag = false; // 如果 flag 值修改后,讓 B 線程先打印信息 TimeUnit.SECONDS.sleep(2);System.out.printf("********** %s 線程執行結束!**********", Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } }, "B").start();}}B 線程修改 flag 值后,對 A 線程數據有效,A 線程跳出循環,執行完成。所以 volatile 修飾的變量,有新值寫入后,對其他線程來說,數據是有效的,能被其他線程讀到。
主內存和工作內存
上面代碼中的變量加了 volatile 修飾,為什么就能被其他線程讀取到,這就涉及到 Java 內存模型規定的變量訪問規則。
主內存:主內存是機器硬件的內存,主要對應Java 堆中的對象實例數據部分。
工作內存:每個線程都有自己的工作內存,對應虛擬機棧中的部分區域,線程對變量的讀/寫操作都必須在工作內存中進行,不能直接讀寫主內存的變量。
上面 無volatile修飾變量部分的代碼執行示意圖如下:
當 A 線程讀取到 flag 的初始值為 true,進行 while 循環操作,B 線程將工作內存 B 里的 flag 更新為 false,然后將值發送到主內存進行更新。隨后,由于此時的 A 線程不會主動刷新主內存中的值到工作內存 A 中,所以線程 A 所取得 flag 值一直都是 true,A 線程也就為死循環不會停止下來。
上面 volatile修飾變量部分的代碼執行示意圖如下:
當 B 線程更新 volatile 修飾的變量時,會向 A 線程通過線程之間的通信發送通知(JDK5 或更高版本),并且將工作內存 B 中更新的值同步到主內存中。A 線程接收到通知后,不會再讀取工作內存 A 中的值,會將主內存的變量通過主內存和工作內存之間的交互協議,拷貝到工作內存 A 中,這時讀取的值就是線程 A 更新后的值 flag=false。整個變量值得傳遞過程中,線程之間不能直接訪問自身以外的工作內存,必須通過主內存作為中轉站傳遞變量值。在這傳遞過程中是存在拷貝操作的,但是對象的引用,虛擬機不會整個對象進行拷貝,會存在線程訪問的字段拷貝。
有序性
volatile 包含禁止指令重排的語義,Java 內存模型會限制編譯器重排序和處理器重排序,簡而言之就是單個線程內表現為串行語義。那什么是重排序?重排序的目的是編譯器和處理器為了優化程序性能而對指令序列進行重排序,但在單線程和單處理器中,重排序不會改變有數據依賴關系的兩個操作順序。比如:
/** * Created by YANGTAO on 2020/3/15 0015. */public class ReorderDemo { static int a = 0;static int b = 0;public static void main(String[] args) { a = 2; b = 3; }} // 重排序后: public class ReorderDemo { static int a = 0;static int b = 0;public static void main(String[] args) { b = 3; // a 和 b 重排序后,調換了位置 a = 2; }}但是如果在單核處理器和單線程中數據之間存在依賴關系則不會進行重排序,比如:
/** * Created by YANGTAO on 2020/3/15 0015. */public class ReorderDemo {static int a = 0;static int b = 0;public static void main(String[] args) { a = 2; b = a; }} // 由于 a 和 b 存在數據依賴關系,則不會進行重排序volatile 實現特有的內存語義,Java 內存模型定義以下規則(表格中的 No 代表不可以重排序):
Java 內存模型在指令序列中插入內存屏障來處理 volatile 重排序規則,策略如下:
volatile 寫操作前插入一個 StoreStore 屏障
volatile 寫操作后插入一個 StoreLoad 屏障
volatile 讀操作后插入一個 LoadLoad 屏障
volatile 讀操作后插入一個 LoadStore 屏障
該四種屏障意義:
StoreStore:在該屏障后的寫操作執行之前,保證該屏障前的寫操作已刷新到主內存。
StoreLoad:在該屏障后的讀取操作執行之前,保證該屏障前的寫操作已刷新到主內存。
LoadLoad:在該屏障后的讀取操作執行之前,保證該屏障前的讀操作已讀取完畢。
LoadStore:在該屏障后的寫操作執行之前,保證該屏障前的讀操作已讀取完畢。
原子性
前面有提到 volatile 的原子性是相對于單個 volatile 變量的讀/寫具有,比如下面代碼:
/** * Created by YANGTAO on 2020/3/15 0015. */public class AtomicDemo {static volatile int num = 0;public static void main(String[] args) throws InterruptedException {final CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { // 創建 10 個線程 new Thread(() -> { for (int j = 0; j < 1000; j++) { // 每個線程累加 1000 num ++; } latch.countDown(); }, String.valueOf(i+1)).start(); }latch.await(); // 所有線程累加計算的數據 System.out.printf("num: %d", num); }}上面代碼中,如果 volatile 修飾 num,在 num++ 運算中能持有原子性,那么根據以上數量的累加,最后應該是 num:10000。代碼執行結果:
結果與我們預計數據的相差挺多,雖然 volatile 變量在更新值的時候回通知其他線程刷新主內存中最新數據,但這只能保證其基本類型變量讀/寫的原子操作(如:num = 2)。由于 num++是屬于一個非原子操作的復合操作,所以不能保證其原子性。
使用場景
volatile 變量最后的運算結果不依賴變量的當前值,也就是前面提到的直接賦值變量的原子操作,比如:保存數據遍歷的特定條件的一個值。
可以進行狀態標記,比如:是否初始化,是否停止等等。
總結
volatile 是一個簡單又輕量級的同步機制,但在使用過程中,局限性比較大,要想使用好它,必須了解其原理及本質,所以在使用過程中遇到的問題,相比于其他同步機制來說,更容易出現問題。但使用好 volatile,在某些解決問題上能獲取更佳的性能。
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,目前,已經拿到了大廠offer,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「Java面試」即可獲取
總結
以上是生活随笔為你收集整理的volatile 手摸手带你解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 多线程启动为什么调用 star
- 下一篇: 注意了,Fastjson 最新高危漏洞来