一、輕量級的同步機制
- 保證可見性
- 禁止指令排序
- 不保證原子性
二、基本概念
- JMM 本身是一種抽象的概念並不是真實存在,它描述的是一組規定或則規範,通過這組規範定義了程序中的訪問方式。
- JMM 同步規定
- 線程解鎖前,必須把共享變量的值刷新回主內存
- 線程加鎖前,必須讀取主內存的最新值到自己的工作內存
- 加鎖解鎖是同一把鎖
- 由於 JVM 運行程序的實體是線程,而每個線程創建時 JVM 都會爲其創建一個工作內存,工作內存是每個線程的私有數據區域,而 Java 內存模型中規定所有變量的儲存在主內存,主內存是共享內存區域,所有的線程都可以訪問,但線程對變量的操作(讀取賦值等)必須都工作內存進行看。
- 首先要將變量從主內存拷貝的自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,工作內存中存儲着主內存中的變量副本拷貝,前面說過,工作內存是每個線程的私有數據區域,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成。
- 內存模型圖
三、特性
-
可見性
public class VolatileDemo { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + " coming..."); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } data.addOne(); System.out.println(Thread.currentThread().getName() + " updated..."); }).start(); while (data.a == 0) { // looping } System.out.println(Thread.currentThread().getName() + " job is done..."); } } class Data { // int a = 0; volatile int a = 0; void addOne() { this.a += 1; } }
如果不加 volatile 關鍵字,則主線程會進入死循環,加 volatile 則主線程能夠退出,說明加了 volatile 關鍵字變量,當有一個線程修改了值,會馬上被另一個線程感知到,當前值作廢,從新從主內存中獲取值。對其他線程可見,這就叫可見性。
-
不可原子性
public class VolatileDemo { public static void main(String[] args) { // test01(); test02(); } // 測試原子性 private static void test02() { Data data = new Data(); for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { data.addOne(); } }).start(); } // 默認有 main 線程和 gc 線程 while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(data.a); } } class Data { volatile int a = 0; void addOne() { this.a += 1; } }
發現並不能輸出 20000
-
有序性
-
計算機在執行程序時,爲了提高性能,編譯器個處理器常常會對指令做重排,一般分爲以下 3 種
- 編譯器優化的重排
- 指令並行的重排
- 內存系統的重排
-
單線程環境裏面確保程序最終執行的結果和代碼執行的結果一致
-
處理器在進行重排序時必須考慮指令之間的數據依賴性
-
多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證用的變量能否一致性是無法確定的,結果無法預測
-
代碼示例
public class ReSortSeqDemo { int a = 0; boolean flag = false; public void method01() { a = 1; // flag = true; // ----線程切換---- flag = true; // a = 1; } public void method02() { if (flag) { a = a + 3; System.out.println("a = " + a); } } }
如果兩個線程同時執行,method01 和 method02 如果線程 1 執行 method01 重排序了,然後切換的線程 2 執行 method02 就會出現不一樣的結果。
-
四、禁止指令排序
volatile 實現禁止指令重排序的優化,從而避免了多線程環境下程序出現亂序的現象
先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個 CPU 指令,他的作用有兩個:
- 保證特定操作的執行順序
- 保證某些變量的內存可見性(利用該特性實現 volatile 的內存可見性)
由於編譯器個處理器都能執行指令重排序優化,如果在指令間插入一條 Memory Barrier 則會告訴編譯器和 CPU,不管什麼指令都不能將這條 Memory Barrier 指令重排序,也就是說通過插入內存屏障禁止在內存屏障前後執行重排序優化。內存屏障另一個作用是強制刷出各種 CPU 緩存數據,因此任何 CPU 上的線程都能讀取到這些數據的最新版本。
下面是保守策略下,volatile寫插入內存屏障後生成的指令序列示意圖:
下面是在保守策略下,volatile讀插入內存屏障後生成的指令序列示意圖:
五、線程安全性保證
- 工作內存與主內存同步延遲現象導致可見性問題
- 可以使用 synchronzied 或 volatile 關鍵字解決,它們可以使用一個線程修改後的變量立即對其他線程可見
- 對於指令重排導致可見性問題和有序性問題
- 可以利用 volatile 關鍵字解決,因爲 volatile 的另一個作用就是禁止指令重排序優化
六、volatile案例
單例
-
多線程環境下可能存在的安全問題
@NotThreadSafe public class Singleton01 { private static Singleton01 instance = null; private Singleton01() { System.out.println(Thread.currentThread().getName() + " construction..."); } public static Singleton01 getInstance() { if (instance == null) { instance = new Singleton01(); } return instance; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(()-> Singleton01.getInstance()); } executorService.shutdown(); } }
發現構造器裏的內容會多次輸出
-
雙重鎖單例
- 代碼
public class Singleton02 { private static volatile Singleton02 instance = null; private Singleton02() { System.out.println(Thread.currentThread().getName() + " construction..."); } public static Singleton02 getInstance() { if (instance == null) { synchronized (Singleton01.class) { if (instance == null) { instance = new Singleton02(); } } } return instance; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(()-> Singleton02.getInstance()); } executorService.shutdown(); } }
-
如果沒有加 volatile 就不一定是線程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
-
原因是在於某一個線程執行到第一次檢測,讀取到的 instance 不爲 null 時,instance 的引用對象可能還沒有完成初始化。
-
instance = new Singleton()
可以分爲以下三步完成
memory = allocate(); // 1.分配對象空間
instance(memory); // 2.初始化對象
instance = memory; // 3.設置instance指向剛分配的內存地址,此時instance != null
-
步驟 2 和步驟 3 不存在依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種優化是允許的。
-
發生重排
memory = allocate(); // 1.分配對象空間
instance = memory; // 3.設置instance指向剛分配的內存地址,此時instance != null,但對象還沒有初始化完成
instance(memory); // 2.初始化對象
- 所以不加 volatile 返回的實例不爲空,但可能是未初始化的實例