在多線程的情況下,對一個值進行 a++ 操作,會出現什么問題?
a++ 的問題
先寫個 demo 的例子。把 a++ 放入多線程中運行一下。定義 10 個線程,每個線程里面都調用 5 次 a++,把 a 用 volatile 修飾,可以讓 a 的值在修改之后,所有的線程立刻就可以知道。最后結果是不是 50,還是其他的數字?
public class Test {
private static volatile int a = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
try {
for(int j = 0; j < 10; j++) {
System.out.print(a++ + ", ");
Thread.sleep(100);
}
} catch (Exception e) {
}
}
});
threads[i].start();
}
}
}
從結果上看 a++ 的操作并沒有達到預期值的 50,而是少了很多,其中還有一定是有問題的。那就是因為 a++ 的操作并不是原子性的。
原子性
并發編程,有三大原則:有序性、可見性、原子性
- 有序性:正常編譯器執行代碼,是按順序執行的。有時候,在代碼順序對程序的結果沒有影響時,編譯器可能會為了性能從而改變代碼的順序。
- 可見性:一個線程修改了一個變量的值,另外一個線程立刻可以知道修改后的值。
- 原子性:一個操作或者多個操作在執行的時候,要么全部被執行,要么全部都不執行。
上面的 a++ 就沒有原子性,它有三個步驟:
- 在內存中讀取了 a 的值。
- 對 a 進行了 + 1 操作。
- 將新的 a 值刷回到內存。
這三個步驟可以被示例中的 10 個線程上下文切換打斷:當 a = 10
- 線程 1 將 a 的值讀取到內存, a = 10
- 線程 2 將 a 的值讀取到內存, a = 10
- 線程 1 將 a + 1,a = 11
- 此時線程發生切換,線程 2 對 a 進行 + 1 操作, a = 11
- 線程 2 將 a 的值寫回到內存, a = 11
- 線程 1 將 a 的值寫回到內存, a = 11
從上面的步驟中可以看出 a 的值在兩次相加后沒有得到 12 的值,而是 11。這就是 a++ 引發的問題。
小 B 把上面的步驟對面試官講了一遍,面試官又問了,有什么方式可以避免這個問題,小 B 不加思索的回答用 synchronized 加鎖。面試官說 synchronized 太重了,還有其他的解決方式嗎?小 B 暈了。其實可以使用 AtomicInteger 的 incrementAndGet() 方法。
AtomicInteger 源碼分析
主要屬性
首先看看 AtomicInteger 的主要屬性。
//sun.misc 下的類,提供了一些底層的方法,用于和操作系統交互
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 字段的內存地址相對于對象內存地址的偏移量
private static final long valueOffset;
//通過 unsafe 初始化 valueOffset,獲取偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 用 valatile 修飾的值,保證了內存的可見性
private volatile int value;
從屬性中可以看出 AtomicInteger 調用的是 Unsafe 類,Unsafe 類中大多數的方法是用 native 修飾的,可以直接進行一些系統級別的操作。
用 volatile 修飾 value 值,保證了一個線程的值對另外一個線程立即可見。
incrementAndGet()
//AtomicInteger.incrementAndGet()
public final int incrementAndGet() {
//調用 unsafe.getAndAddInt()
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//Unsafe.getAndAddInt()
//參數:需要操作的對象,偏移量,要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//Unsafe.compareAndSwapInt()
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
incrementAndGet() 首先獲取了當前值,然后調用 compareAndSwapInt() 方法更新數據。
compareAndSwapInt() 是 CAS 的縮寫來源,比較并替換。被 native 修飾,調用了操作系統底層的方法,保證了硬件級別的原子性。
var2,var4,var5 是它的三個操作數,表示內存地址偏移量 valueOffset,預期原值 expect,新的值 update。把 this.compareAndSwapInt(var1, var2, var5, var5 + var4)
變成 this.compareAndSwapInt(obj, valueOffset, expect, update)
,釋義就是如果內存位置中的 valueOffset 值 與 expect 的值相同,就把內存中的 valueOffset 改成 update,否則不操作。
getAndAddInt() 方法中用了 do-while,就相當于如果 CAS 一直更新不成功,就不退出循環。直到更新成功為止。
ABA 問題
CAS 操作也并不是沒有問題的。
- 循環操作時間長了,開銷大。用了 do-while,如果更新一直不成功,就一直在循環。會給 CPU 帶來很大的開銷。
- 只能保證一個共享變量的原子性。循環 CAS 的方式只能保證一個變量進行原子操作,在對多個變量進行 CAS 的時候就沒辦法保證原子性了。
- ABA 問題。CAS 的操作一般是 1. 讀取內存偏移量 valueOffset。2. 比較 valueOffset 和 expect 的值。3. 更新 valueOffset 的值。如果線程 A 讀取 valueOffset 后,線程 B 修改了 valueOffset 的值,并且將 valueOffset 的值又改了回來。線程 A 會認為 valueOffset 的值并沒有改變。這就是 ABA 問題。要解決這個問題,就是在每次修改 valueOffset 值的時候帶上一個版本號。
總結
這篇文章介紹了 CAS,它是 java 中的樂觀鎖,每次認為操作并不會有其他線程去修改數據,如果有其他線程操作了數據,就重試,一直到成功為止。
-
編程
+關注
關注
88文章
3637瀏覽量
93989 -
多線程
+關注
關注
0文章
278瀏覽量
20075 -
代碼
+關注
關注
30文章
4828瀏覽量
69058
發布評論請先 登錄
相關推薦
評論