本篇文章我們來講java.util.concurrent包下的原子變量,原子變量的引入主要是爲了解決普通變量(int、Integer、Long等)修改操作不是原子的,進而導致必須使用同步機制才能保證安全更新的問題。舉個例子:
public class Counter {
private int count;
public void incr(){
count ++;
}
public int getCount() {
return count;
}
}
Counter類有兩個方法,incr方法用於對成員變量count自增加1,getCount方法用於獲取count值,通過如下方式使用:
public class Test {
public static class Counter {
private int count;
public void incr() {
count++;
}
public int getCount() {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10; j++) {
counter.incr();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
for (int i = 0; i < 5; i++) {
threads[i].join();
}
System.out.println(counter.getCount());
}
}
main方法中,開啓五個線程,每個線程的調用counter的incr方法10次,那麼理論上最後counter調用getCount()方法獲取的結果是50。但是通過多次執行發現,結果不一定是50,有時候可能是49,有時候可能是48。原因在之前講synchronized使用的那篇文章講過,因爲++操作不是原子的。一個++操作涉及三個動作:讀取、加1、回寫。那麼就有可能導致多線程執行時,多個線程讀取到同一個值,導致最終count的值小於50。
解決辦法之前也講過,就是把incr和getCount方法聲明爲synchronized方法。但是對於count++這種操作來說,使用synchronzied成本太高了(雖然synchronized在Java6之後進行了一系列優化),獲取鎖、釋放鎖以及相關的上下文切換等,這些都需要成本。
對於這種情況,完全可以使用原子變量代替,Java併發包中的基本原子變量類型有:
- AtomicBoolean:原子Boolean類型
- AtomicInteger:原子Integer類型
- AtomicLong:原子Long類型
- AtomicReference:原子引用類型
針對Integer,Long和Reference類型,還有對應的數組類型:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
原子方式修改對象內部成員變量類型:
- AtomicReferenceFieldUpdater
AtomicReference還有兩個類似的類,在某些情況下更爲易用:
- AtomicMarkableReference
- AtomicStampedReference
可以發現,沒有針對char,short,float,double類型的原子變量。有可能是因爲這幾種類型使用的相對較少吧。如果需要,可以轉換爲int/long,然後使用AtomicInteger或AtomicLong。比如,對於float,可以使用Float類如下方法和int相互轉換:
public static int floatToIntBits(float value)
public static float intBitsToFloat(int bits)
1. AtomicInteger
1.1 基本方法說明
S.N. | 方法 | 說明 |
1 | public AtomicInteger(int initialValue) | 構造函數,給定初始值 |
2 | public AtomicInteger() | 構造函數,初始值爲0 |
3 | public final int get() | 獲取AtomicInteger中的值 |
4 | public final void set(int newValue) | 設置AtomicInteger中的值 |
5 | public final boolean compareAndSet(int expect, int update) | CAS,expect爲期望的值,update爲修改的值 |
6 | public final int getAndSet(int newValue) | 以原子方式設置新值,並返回舊值 |
7 | public final int getAndIncrement() | 以原子方式將當前值加1,並返回舊值 |
8 | public final int getAndDecrement() | 以原子方式將當前值減1,並返回舊值 |
9 | public final int getAndAdd(int delta) | 以原子方式將當前值加delta,並返回舊值 |
10 | public final int incrementAndGet() | 以原子方式將當前值加1,並返回新值 |
11 | public final int decrementAndGet() | 以原子方式將當前值減1,並返回新值 |
12 | public final int addAndGet(int delta) | 以原子方式將當前值加delta,並返回新值 |
13 | public final int getAndUpdate(IntUnaryOperator updateFunction) | Java8新方法,以原子方式將當前值更新爲函數結果,並返回舊值 |
14 | public final int updateAndGet(IntUnaryOperator updateFunction) | Java8新方法,以原子方式將當前值更新爲函數結果,並返回新值 |
15 | public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) | Java8新方法,以原子方式將當前值更新爲函數結果,並返回舊值 |
16 | public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) | Java8新方法,以原子方式將當前值更新爲函數結果,並返回新值 |
這裏重點提一下compareAndSet方法,該方法以原子方式實現瞭如下功能:如果當前值等於expect,則更新爲update,否則不更新,如果更新成功,返回true,否則返回false。(依賴於Unsafe類,Unsafe類的方法都試native方法)
AtomicInteger可以在程序中用作一個計數器,多個線程併發更新,也總能實現正確性,比如上面的例子改爲:
public class Test1 {
public static class Counter {
private AtomicInteger count;
public Counter(AtomicInteger count) {
this.count = count;
}
public void incr() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(new AtomicInteger());
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10; j++) {
counter.incr();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
for (int i = 0; i < 5; i++) {
threads[i].join();
}
System.out.println(counter.getCount());
}
}
因爲incrementAndGet()方法是原子的,所以程序總能輸出正確的結果50,雖然沒有使用synchronized同步。
1.2 AtomicInteger基本原理
AtomicInteger類聲明爲:
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//Methods
}
Unsafe類提供Java直接訪問系統內存資源、自主管理內存的入口,提供了硬件級別的原子操作,使Java語言擁有了類似C語言指針一樣操作內存空間的能力。AtomicInteger類中的Unsafe成員變量,就是用來操作成員變量value的內存空間的(實現原子方式改變value值)。
成員變量value就是用來存儲值的,並且聲明爲volatile,所以可以保證可見性。而value訪問的的原子性就是通過上述unsafe成員變量實現的。
1.2.1 incrementAndGet
AtomicInteger類中大部分更新方法實現都很類似,我們先來分析一下incrementAndGet實現:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//var5爲當前值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Unsafe類的getAndAddInt方法,就是一直執行CAS操作,將當前值var5改爲var5 + var4,並返回更改前的舊值var5。
與synchronized鎖相比,這種原子更新方式代表一種不同的思維方式。
- synchronized是悲觀的,它假定更新很可能衝突,所以先獲取鎖,得到鎖後才更新。原子變量的更新邏輯是樂觀的,它假定衝突比較少,但使用CAS更新,也就是進行衝突檢測,如果確實衝突了,那就繼續嘗試使用CAS更新
- synchronized代表一種阻塞式算法,得不到鎖的時候,進入鎖等待隊列,等待其他線程喚醒,有上下文切換開銷。原子變量的更新邏輯是非阻塞式的,更新衝突的時候,它就重試,不會阻塞,不會有上下文切換開銷
對於大部分比較簡單的操作,無論是在低併發還是高併發情況下,這種樂觀非阻塞方式的性能都要遠高於悲觀阻塞式方式。
1.2.2 compareAndSet
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe類中compareAndSwapInt方法是native方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5)
一般的計算機系統都在硬件層次上直接支持CAS指令,compareAndSwapInt肯定也是通過這些指令實現的CAS。關於Unsafe類,美團技術博客有篇文章講述的很清楚,Java魔法類:Unsafe應用解析,感興趣的同學可以去了解一下。
1.2.3 getAndUpdate(IntUnaryOperator updateFunction)
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
//將prev應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
//返回舊值
return prev;
}
1.2.4 updateAndGet(IntUnaryOperator updateFunction)
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
updateAndGet方法實現跟getAndUpdate一致,唯一不同的是updateAndGet方法返回的是修改後的值。
1.2.5 getAndAccumulate
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
//將prev和參數x應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
//返回舊值
return prev;
}
1.2.6 accumulateAndGet
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
//將prev和參數x應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
//返回新值
return next;
}
1.3 AtomicInteger應用
AtomicInteger最直觀的應用肯定是計數器,其實AtomicInteger的CAS屬性,除了可以實現樂觀非阻塞算法,它也可以用來實現悲觀阻塞式算法,比如鎖。實際上,Java併發包中的所有阻塞式工具、容器、算法也都是基於CAS的。下面代碼展示使用AtomicInteger實現一個鎖,如下:
public class MyLock {
private AtomicInteger status = new AtomicInteger(0);
public void lock() {
while (!status.compareAndSet(0, 1)) {
Thread.yield();
}
}
public void unlock() {
status.compareAndSet(1, 0);
}
}
在MyLock中,使用status表示鎖的狀態,0表示未鎖定,1表示鎖定,lock()/unlock()使用CAS方法更新,lock()只有在更新成功後才退出,實現了阻塞的效果,不過一般而言,這種阻塞方式過於消耗CPU。MyLock只是用於演示基本概念,實際開發中應該使用Java併發包中的類如ReentrantLock。
2. AtomicBoolean/AtomicLong/AtomicReference
AtomicBoolean/AtomicLong/AtomicReference的用法和原理與AtomicInteger非常類似,下面簡單介紹一下。
2.1 AtomicBoolean
AtomicBoolean類聲明如下:
public class AtomicBoolean implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//Methods
}
可以看到AtomicBoolean類內部的值value並不是boolean類型的,而實int類型,用1表示true, 0表示false,比如,其CAS方法實現爲:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
就是把boolean值轉化爲int值,然後通過Unsafe類的compareAndSwapInt方法,以原子方式替換value值。
2.2 AtomicLong
AtomicLong可以用來在程序中生成唯一序列號,它的方法與AtomicInteger類似,就不贅述了。它的CAS方法調用的是unsafe的另一個方法,如:
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
原理是同AtomicInteger一樣的,使用方式也一致,這裏不多講了。
2.3 AtomicReference
AtomicReference用來以原子方式更新複雜類型,它有一個類型參數,使用時需要指定引用的類型。以下代碼演示了其基本用法:
public class AtomicReferenceDemo {
static class Pair {
final private int first;
final private int second;
public Pair(int first, int second) {
this.first = first;
this.second = second;
}
public int getFirst() {
return first;
}
public int getSecond() {
return second;
}
}
public static void main(String[] args) {
Pair p = new Pair(100, 200);
AtomicReference<Pair> pairRef = new AtomicReference<>(p);
pairRef.compareAndSet(p, new Pair(200, 200));
System.out.println(pairRef.get().getFirst());
}
}
AtomicReference的CAS方法調用的是unsafe的另一個方法:
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
3. 原子數組
原子數組方便以原子的方式更新數組中的每個元素,下面以AtomicIntegerArray爲例來簡要介紹下,AtomicLongArray、AtomicReferenceArray類似。
3.1 構造方法
public AtomicIntegerArray(int length)
public AtomicIntegerArray(int[] array)
第一個會創建一個長度爲length的空數組;第二個構造函數接受一個已有的數組,但不會直接操作該數組,而是會創建一個新數組,只是拷貝參數數組中的內容到新數組,對AtomicIntegerArray內容的修改不會影響原數組。
3.2 方法說明
AtomicIntegerArray中的原子更新方法大多帶有數組索引參數,如下:
public final boolean compareAndSet(int i, int expect, int update)
public final int getAndIncrement(int i)
public final int getAndAdd(int i, int delta)
含義跟AtomicInteger相同,不同的是AtomicIntegerArray中的方法要上送數組的index,以原子方式修改數組特定index位置的元素。
3.3 使用示例
public class AtomicArrayDemo {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4 };
AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);
atomicArr.compareAndSet(1, 2, 100);
System.out.println(atomicArr.get(1));
System.out.println(arr[1]);
}
}
運行結果:
100
2
4. AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater方便以原子方式更新對象中的成員變量,成員變量不需要聲明爲原子變量,AtomicReferenceFieldUpdater是基於反射機制實現的。下面看一個簡單的使用示例:
public class FieldUpdaterDemo {
static class DemoObject {
private volatile int num;
private volatile Object ref;
private static final AtomicIntegerFieldUpdater<DemoObject> numUpdater
= AtomicIntegerFieldUpdater.newUpdater(DemoObject.class, "num");
private static final AtomicReferenceFieldUpdater<DemoObject, Object>
refUpdater = AtomicReferenceFieldUpdater.newUpdater(
DemoObject.class, Object.class, "ref");
public boolean compareAndSetNum(int expect, int update) {
return numUpdater.compareAndSet(this, expect, update);
}
public int getNum() {
return num;
}
public Object compareAndSetRef(Object expect, Object update) {
return refUpdater.compareAndSet(this, expect, update);
}
public Object getRef() {
return ref;
}
}
public static void main(String[] args) {
DemoObject obj = new DemoObject();
obj.compareAndSetNum(0, 100);
obj.compareAndSetRef(null, new String("hello"));
System.out.println(obj.getNum());
System.out.println(obj.getRef());
}
}
5. AtomicStampedReference
使用CAS方式更新有一個ABA問題,一個線程開始看到的值是A,隨後使用CAS進行更新,它的實際期望是沒有其他線程修改過才更新,但普通的CAS做不到,因爲可能在這個過程中,已經有其他線程修改過了,比如先改爲了B,然後又改回爲了A。
ABA是不是一個問題與程序的邏輯有關,如果是一個問題,一個解決方法是使用AtomicStampedReference,在修改值的同時附加一個時間戳,只有值和時間戳都相同才進行修改,其CAS方法實現爲:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
AtomicStampedReference在compareAndSet中要同時修改兩個值,一個是引用,另一個是時間戳,通過上述代碼,可以看到,AtomicStampedReference內部會將這兩個值封裝爲Pair對象,並通過UNSAFE.compareAndSwapObject以原子方式更新。
使用示例:
public static void main(String[] args) {
Pair<Integer, Integer> pair = new Pair<>(100, 200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<Pair>(pair, stamp);
int newStamp = 2;
pairRef.compareAndSet(pair, new Pair<>(200, 200), stamp, newStamp);
}
6. AtomicMarkableReference
AtomicMarkableReference是另一個AtomicReference的增強類,與AtomicStampedReference類似,它也是給引用關聯了一個字段,只是這次是一個boolean類型的標誌位,只有引用值和標誌位都相同的情況下才進行修改,同樣可以解決ABA問題。來看一下CAS實現:
public boolean compareAndSet(V expectedReference,
V newReference,
boolean expectedMark,
boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
casPair(current, Pair.of(newReference, newMark)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
可以看到跟AtomicStampedReference實現方式一致,唯一的區別是標誌位不同,AtomicStampedReference中使用的是int型變量,AtomicMarkableReference中使用的是boolean變量。
以上就是原子變量的所有內容,原子變量可以保證在無鎖的環境下原子的改變變量,但是也存在一些侷限性,如下:
- ABA問題:普通的CAS操作檢查值有沒有發生變化,如果沒發生變化則更新,但是如果一個值一開始是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號,在變量前追加上版本號,每次更新的時候把版本號加1,那麼A -> B -> A就會編程1A -> 2B -> 3A。Java API atomic包內的AtomicStampedReference和AtomicMarkableReference都使用類似的思路解決了ABA問題。
- 循環時間長開銷大:類似於incrementAndGet方法都使用了自旋CAS,如果自旋長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令,那麼效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,有些處理器上延遲時間是零;第二,他可以避免在退出循環的時候因內存順序衝突(Memory Order Violantion)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU執行效率。
- 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用CAS方式保證原子性,但是對多個共享變量操作時,就無法保證操作的原子性了,這時候還是需要使用鎖。還有一個解決辦法就是,把多個共享變量合成一個共享變量,然後通過AtomicReference類來原子更新合併的共享變量。
參考鏈接:
1. 《Java編程的邏輯》
2. Java API
3. 《Java併發編程的藝術》