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