線程安全之可見性(Volatile)和原子性
線程之可見性
可見性原理分析
可見性問題:讓一個線程對共享變量的修改,能夠及時的被其他線程看到。
JAVA 內存模型規定:
對volatile變量v的寫入,與所有其他線程後續對v的讀同步
要滿足這些條件,所以volatile 關鍵字就有以下的功能:
- 禁止緩存;
volatile 變量訪問控制符會加個ACC_VOLATILE
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2 - 對volatile 變量相關的指令不做重排序。
共享變量的定義
可以在線程之間共享的內存稱爲共享內存或堆內存。
所有實例字段,靜態字段和數組元素都存在在堆內存中,這些字段和數組都是共享變量
衝突發生:如果至少有一個訪問是寫操作,那麼對同一個變量的兩次訪問是衝突的。
這些能被多個線程訪問的共享變量時內存模型規範的對象。
線程間操作的定義
1.線程間的操作是指:一個程序執行的操作可被其他線程感知或被其他線程直接影響。
2. Java 內存模型只描述線程間操作,不描述線程內操作,線程內操作按照線程內語義執行。線程間的操作有:
read 一般讀操作,非volatile讀
write 一般寫操作,非volatile寫
volatile read
volatile write
Lock.(鎖monitor)、Unlock
線程的第一個和最後一個操作
外部操作
所有線程間的操作都存在可見性問題,JMM需要對其進行規範
同步的規則定義
對volatile變量v的寫入,與所有其他線程後續對v的讀同步。
對於監視器m的解鎖與所有後續操作對於m的加鎖同步。
Happens-before 先行發生原則
happens before 用於描述兩個有衝突的動作之間的順序,如果有一個action happends before 另一個action,則第一個操作被第二個操作可見,JVM 需要實現如下happens-before 規則:
某個線程中的每個動作都happens-before 該線程後面的動作。
某個管程上的unlock 動作和happens-before 同一個管程上後續的lock動作。
對某個volatile字段的寫操作happens-before 每個後續對該volatile字段的讀操作。
在某個線程對象上調用start() happens-before 被啓動線程中的任意動作。
如果在線程t1 中成功執行了t2.join(),則t2 中的所有操作對t1可見。
如果某個動作a happens-before動作b,且b happens-before 動作c,則有 a happens-before c。
當程序包含兩個沒有被happens-before關係排序的衝突訪問時,就稱存在數據競爭,遵守了這個原則,也就意味着有些代碼不能進行重排序,有些數據不能緩存!
Final 修飾符
final 在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final 字段的正確構造版本。僞代碼示例: f = new FinalDemo(); 讀取到的f.x 一定是最新的值,x 爲final 修飾的字段。
如果在構造函數中設置字段後發生讀取,則會看到該final 字段分配的值,否則它將看到默認值:僞代碼示例:pulbic finalDemo(){x=1;y=x} y會等於1;
Word Tearing 字節處理
有些處理器(尤其是早起的Alphas處理器)沒有提供些單個字節的功能,在這樣的處理器上更新byte數組,若只是簡單的讀取整個內容,更新對象的字節,然後將這個內容再寫回內存,將是不合法的。
這個問題有時被分爲“字分裂(word tearing)”更新單個字節有難度的處理器,就需要尋求其它方式來解決問題。因此在開發找那個儘量不要對byte[] 中的元素進行重新賦值,更不要在多線程程序這樣做。
線程可見性總結
保證變量可見性的方式:
-
final變量
-
synchronized 關鍵字:
synchronized 語義規範:
1) 進入同步代碼塊前,先清空工作內存中的所有共享變量,從主內存中重新加載。
2) 解鎖前必須把修改的共享變量同步回主內存。
注意:如果線程競爭兩把不同的鎖則不能保證
如何保證線程安全:
1) 鎖機制保護共享資源,只有獲得鎖的線程纔可操作共享資源。
2) Synchronized 語義規範保證了修改共享資源後,會同步主內存,就做到了線程安全。 -
用volatile修飾
1) 使用volatile變量時,必須重新從主內存加載,並且read,load是連續的。
2) 修改volatile變量時,必須立馬同步回主內存,並且store,write是連續的。
線程安全之原子性
原子操作
原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分(不可中斷性)。
將整個操作視爲一個整體,資源在該次操作中保持一致,這是原子性的核心特性。
存在的問題及分析原因
示例代碼:
/**
* 實現累加的功能
*/
public class Counter {
volatile int i = 0;
public synchronized void add() {
i++;
}
}
/**
* 對累加器的測試類
*/
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000L);
//期望的結果是:i*j 爲循環次數 = 80000
System.out.println(counter.i);
}
}
輸出的結果:
done...
done...
done...
done...
done...
done...
done...
done...
27661
存在的問題
輸出結果最終是: 27661 並不是我們所期望的80000。再多運行幾次呢,輸出的結果也是每次都不同,但都不是我們想要的值。那麼我們想到可能線程併發的問題,我們將累加器加上鎖或者加上synchronized 關鍵字試試看:
/**
* 加synchronized 的 Counter
*/
public class Counter {
volatile int i = 0;
// 已加上同步關鍵字
public synchronized void add() {
i++;
}
}
/**
* 加鎖的Counter
*/
public class Counter {
volatile int i = 0;
Lock lock = new ReentrantLock();
//已加上鎖
public void add() {
lock.lock();
i++;
lock.unlock();
}
}
再使用上面的CounterTest 分別調用不同寫法的Counter 類,發現運行結果都是我們期望的80000:
done...
done...
done...
done...
done...
done...
done...
done...
80000
這就說明以上我們的猜測是正確的,但是爲什麼會出現不同的結果呢?接下來以下分析:
首先我們還是將Counter 類還原到最初的狀態(不加鎖也不加synchronized):
然後進入到Counter.class 所在的目錄打開終端執行(反編譯查看字節碼):
javap -v -p Counter.class
字節碼顯示:
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
將以上的指令碼對應內存分析,假如現在有t1 線程,按照從上到下的指令碼執行,無論加多少次,結果是我們所期望的。
以上是隻有一個線程的情況,現在又加入了一個線程t2, 按照同樣的指令碼從上到下執行,t1,t2 執行完一次後 i 的結果是1而不是2,前面講到原子操作,是一個不可中斷的操作。 按照下圖可以知道,對於t1線程來說,從上到下執行的指令碼應爲一個原子操作,不可讓其它線程同時執行。由於前面提到可以使用鎖機制來解決這一系列問題,但是現有另一種實現反方式!
解決辦法
藉助sun.misc.Unsafe 類:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
private static long valueOffset;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field declaredField_i = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(declaredField_i);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 自旋
*/
public void add() {
for (; ; ) {
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {
break;//如果修改不成功就自旋,修改成功就退出循環
}
}
}
使用CounterTest 類來創建CounterUnsafe 對象作爲counter 來調用add():
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
//創建CounterUnsafe 對象:
CounterUnsafe counter = new CounterUnsafe();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000L);
//最終輸出的結果是我們期望的結果:80000
System.out.println(counter.i);
}
}
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {...}
如果您覺得以上條件判斷眼熟,那麼您一定了解過:java.util.concurrent.atomic (併發原子類)
CAS(Compare and swap)
Compare and swap 比較和交換。屬於硬件同步原語,處理器提供了基本內存操作的原子性保證。
CAS 操作需要輸入兩個值,一個舊值A(期望操作前的值)和一個新值B, 在操作期間先對舊值進行比較,若沒有發生變化,才交換成新值,發生了變化則不交換。
JAVA 中的sun.misc.Unsafe 類,提供了compareAndSwapInt() 和compareAndSwapLong() 等幾個方法實現CAS。
CAS 原理
同時有多個操作去修改同一個值時,先比較內存中期望的值是否爲CAS中的第一個參數,如果是則用新值替換舊值,如果不是則不修改(自旋)。
CAS 操作的缺點
- 循環+cas ,自旋的實現讓所有線程都處於高頻運行,爭搶CPU執行時間的狀態。如果操作長時間不成功,會帶來很大的CPU資源消耗。
- 僅針對單個變量的操作,不能用於多個變量來實現原子操作。
- ABA 問題。
ABA 問題
線程1,線程2同時執行CAS(0,1)操作,將i 的值由0 改爲1
假設線程1操作成功,線程2操作失敗
緊接着線程1執行了CAS(1,0),將i 的值改回0
ABA 問題的解決辦法
在每次CAS操作中帶上版本號,作爲每次的操作的區分。AtomicStampedReference 中的源碼,initialStamp爲初始化的版本號: