一、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
