一、簡介
happen before
是時鐘順序的先後,並不能保證線程交互的可見性。
即:存在某線程對副本操作,但對於其他線程都是不可見的。
可見性:指某線程修改共享變量的指針對其他線程來說都是可見的,它反映的是指令執行的實時透明度
每個線程都有獨佔的內存區域,如操作棧、本地變量表等
線程本地內存保存了引用變量在堆內存中的副本,線程對變量的所有操作都在本地內存區域中進行,執行結束後再同步到堆內存中。
線程執行或線程切換都是納秒級的。
volatile
: 揮發,不穩定。
當使用
volatile
修飾變量時,意味着任何對此變量的操作都會在內存中進行,不會產生副本,以保證共享變量的可見性,局部阻止了指令重排的發生。
volatile
解決的是多線程共享變量的可見性問題,類似於 synchronized
,但不具備sychronized
的互斥性
二、案例
(1)雙檢查鎖(Double-checked Locking)
如下代碼:
class LazyInitDemo {
private static TransactionService service = null;
public static TransactionService getTransactionService() {
if (service == null) {
// 或者 TransactionService.class
synchronized (LazyInitDemo.class) {
if (service == null) {
service == new TransactionService();
}
}
}
return service;
}
}
調用getTransactionService()
可能會得到初始化未完成的對象
原因:與 JVM 的編譯優化有關
線程1 執行new TransactionService()
, 構造方法還未被調用,編譯器僅僅爲該對象分配了內存空間並設爲默認值
線程2 調用getTransactionService()
,由於service != null
,但是此時service
對象還沒有被賦予真正有效的值,從而無法取到正確的service
單例對象。
解決方法:加上volatile
private static volatile TransactionService service = null;
(2)volatile
不具互斥性
public class VolatileNotAtomic {
private static volatile long count = 0L;
private static final int NUMBER = 10000;
public static void main(String[] args) {
Thread subtractThread = new SubtractThread();
subtractThread.start();
for (int i = 0; i < NUMBER; ++i) {
count ++;
}
// 等待減法線程結束
while (subtractThread.isAlive()){}
System.out.println("count 最後的值: " + count);
}
private static class SubtractThread extends Thread {
@Override
public void run() {
for (int i = 0; i < NUMBER; ++i) {
count --;
}
}
}
}
結果基本不爲0, 因爲--
與++
並不是原子操作。
字節碼如下:
// 1. 讀取 count 並壓入操作棧頂
GETSTATIC count: I
// 2. 常量 1 壓入操作棧頂
ICONST_1
// 3. 取出最頂部兩個元素進行相加
IADD
// 4. 將剛纔得到的和賦值給 count
PUTSTATIC count: I
解決方案:針對--
與++
,可以加鎖,如sychronized
三、實際場景
- 能實現
count++
原子操作的其他類有:AtomicLong
和LongAdder
JDK8 推薦使用
LongAdder
類,它比AtomicLong
性能更好,有效地減少了樂觀鎖的重試次數
- 一讀多寫的併發場景,使用
volatile
修飾變量則非常合適
最典型的應用:
CopyOnWriteArrayList