AtomicInteger一個是專門被設(shè)計用來線程安全地更新Integer的類堂竟。為什么我們需要這個類呢郁竟?為什么不能僅僅就用一個volatile int ?我們能如何使用AtomicInteger?
為什么要用AtomicInteger?
下面展示了一個使用volatile int的非線程安全的counter例子:
public class CounterNotThreadSafe {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
你可以從GitHub下載示例源碼
我們把計數(shù)存在第二行的這個volatile int中州胳。我們需要volatile這個關(guān)鍵詞來確保所有線程總是能獲取到當(dāng)前的實(shí)際值,正如更多細(xì)節(jié)中描述的钦勘。我們在第四行使用++操作來給counter增加計數(shù)陋葡。為了確認(rèn)這個類是否是線程安全的,我們使用了如下的測試:
public class ConcurrencyTestCounter {
private final CounterNotThreadSafe counter = new CounterNotThreadSafe();
@Interleave
private void increment() {
counter.increment();
}
@Test
public void testCounter() throws InterruptedException {
Thread first = new Thread(() -> {increment();});
Thread second = new Thread(() -> {increment();});
first.start();
second.start();
first.join();
second.join();
assertEquals(2, counter.getCount());
}
}
我們需要兩個線程來測試counter是否是線程安全的彻采,分別在第9和第10行創(chuàng)建腐缤。
我們在11和12行啟動這兩個線程。然后肛响,在13和14行使用thread.join()等到兩個線程都結(jié)束岭粤。在兩個線程都結(jié)束后,在15行檢驗(yàn)是否counter的值是2特笋。
為了使所有的線程并發(fā)地測試剃浇,在第三行使用了來自vmlens的InterLeave注解。
Interleave注解會告知vmlens測試被注解的方法時所有的線程交替執(zhí)行猎物。運(yùn)行該測試虎囚,我們會看到這樣的錯誤:
ConcurrencyTestCounter.testCounter:22 expected:<2> but was:<1>
出現(xiàn)這個錯誤是由于++操作并非是原子性的,這兩個線程會互相覆蓋對方的運(yùn)算結(jié)果蔫磨。從vmlens的報告中我們可以知道:
在這種錯誤下淘讥,兩個都線程首先都并行地讀取變量值,然后都對變量進(jìn)行了寫堤如,這就導(dǎo)致了錯誤值1蒲列。
要解決這個bug,可以使用AtomicInteger類:
public class CounterUsingIncrement {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
在第2行搀罢,我們將用AtomicInteger而非int來定義變量count蝗岖。在第4行使用incrementAndGet代替++操作。
現(xiàn)在榔至,由于方法inncrementAndGet()是原子性的抵赢,其他的線程總是在這個方法調(diào)用之前或之后獲取到值,線程們不會覆蓋其他線程的計算結(jié)果唧取。所以铅鲤,在多線程并行下,現(xiàn)在count的值一直都是2兵怯。
如何使用AtomicInteger
AtomicInteger有多種方法允許我們原子地更新AtomicInteger變量。例如腔剂,increment()方法原子地增加AtomicInteger變量媒区,decrementAndGet()原子地減少AtomicInteger變量。
但是compareAndSet()方法比較特別,這個方法允許我們原子地實(shí)現(xiàn)任意計算袜漩。compareAndSet()方法有兩個參數(shù)绪爸,一個是預(yù)期值,一個是更新值宙攻。這個方法原子地檢查當(dāng)前值是否等于預(yù)期值奠货,如果是的話,這個方法會將值替換成更新值并返回true座掘;如果不是的話递惋,保留當(dāng)前值不變并返回false。
使用這個方法的目的是讓compareAndSet()檢查是否當(dāng)前值是否在我們計算新值的時候被其他線程修改了溢陪。如果沒有的話我們可以安全地更新當(dāng)前值萍虽。否則,我們需要用當(dāng)前被修改過的值重新計算新的值形真。
下面的例子展示了如何使用compareAndSet()來實(shí)現(xiàn)我們的counter:
public void increment() {
int current = count.get();
int newValue = current + 1;
while(!count.compareAndSet(current, newValue)) {
current = count.get();
newValue = current + 1;
}
}
在第2行我們先讀取當(dāng)前的值杉编,然后在第3行計算新的值。然后咆霜,在第4行我們使用compareAndSet()檢查是否有另一個線程已經(jīng)修改了當(dāng)前值邓馒。如果當(dāng)前值沒有被修改過,compareAndSet()會更新當(dāng)前值并返回true蛾坯。否則返回false光酣。由于這個測試可能會失敗多次,我們需要使用一個while循環(huán)偿衰。如果這個值被其他線程改變了挂疆,我們需要獲取獲取被修改過的當(dāng)前值(在第5行),然后計算出新的值(第6行)并嘗試再次進(jìn)行更新下翎。
結(jié)論
AtomicInteger使我們可以以線程安全的方式更新integer變量缤言。使用incrementAndGet()或者decrementAndGet()這些方法來做簡單類型的計算。使用get()和compareAndSet用于所有其他類型的計算视事。