一、volatile特性:
volatile是Java虛擬機提供的輕量級的同步機制。主要有三大特性:
- 保證可見性
- 不保證原子性
- 禁止指令重排序
1、保證可見性
1)代碼演示
AAA線程修改變量number的值為60,main線程獲取到的number值是0,就一直循環等待。
原因:int number = 0;number變量之前沒有添加volatile關鍵字,沒有可見性。添加volatile關鍵字,可以解決可見性問題。
public class VolatileDemo { int number = 0; public void addTo60() { this.number = 60; } //volatile可以保證可見性,及時通知其他線程,主物理內存的值已經被修改 public static void main(String[] args) { VolatileDemo volatileDemo = new VolatileDemo(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + " come in"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } volatileDemo.addTo60(); System.out.println(Thread.currentThread().getName() + " update number value:" + volatileDemo.number); }, "AAA").start(); //第2個線程是main線程 while (volatileDemo.number == 0) { //main線程就一直等待循環,直到number的值不等於0 } System.out.println(Thread.currentThread().getName() + " mission is over, main thread number value:" + volatileDemo.number); } }
2)volatile是如何來保證可見性的呢?
如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令。
- 將這個變量所在緩存行的數據寫回到系統內存。
- 這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。
2、不保證原子性
1)代碼演示
volatile修飾number,進行number++操作,每次執行number的返回結果都不一樣
public class VolatileDemo { volatile int number = 0; public void increase() { number++; } public static void main(String[] args) { VolatileDemo volatileDemo = new VolatileDemo(); for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { volatileDemo.increase(); } }).start(); } // 默認有 main 線程和 gc 線程 while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + " finally number value:" + volatileDemo.number); } }
2)volatile為什么不保證原子性?
number++被拆分成3個指令:
getfield 從主內存中拿到原始值
iadd 在線程工作內存中進行加1操作
putfield 把累加后的值寫回主內存
如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取number的值,
那么第二個線程就會與第一個線程看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗。
3)如何解決原子性問題
- CAS機制:AtomicInteger number = AtomicInteger(0)
- 鎖機制:synchronized、Lock
3、禁止指令重排序
volatile的寫-讀與鎖的釋放-獲取有相同的內存效果。
volatile寫-讀的內存語義:
當寫一個volatile變量時,JMM會把線程A對應的本地內存中的共享變量值刷新到主內存。
當讀一個volatile變量時,JMM會把線程B對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
線程A寫一個volatile變量,隨后線程B讀這個volatile變量,實質上是線程A通過主內存向線程B發送消息。
public class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) { System.out.println("resultValue:" + a); } } }
為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序
volatile寫插入內存屏障:
volatile讀插入內存屏障:
二、你在哪些地方用到過volatile
1、單例模式(雙重檢查鎖DCL)
以下代碼不一定線程安全,原因是有指令重排序的存在,某個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化
因為instance = new SingletonDemo();可以分為以下3步完成(偽代碼)
memory = allocate(); //1.分配對象內存空間
instance(memory); //2.初始化對象
instance = memory; //3.設置instance指向剛分配的內存地址,此時instance!=null
步驟2和步驟3間可能會重排序
使用volatile禁止指令重排序,對volatile變量的寫操作都先行發生於后面對這個變量的讀操作
public class SignletonDemo { private static SignletonDemo instance; private SignletonDemo() { System.out.println(Thread.currentThread().getName() + " 構造方法SingletonDemo"); } public static SignletonDemo getInstance() { //第一次檢測 if (instance == null) { //同步 synchronized (SignletonDemo.class) { if (instance == null) { //多線程環境下可能會出現問題的地方 instance = new SignletonDemo(); } } } return instance; } }
2、讀寫鎖手寫緩存
3、CAS JUC包中大量使用volatile