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