本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
從本節開始,我們探討Java併發工具包java.util.concurrent中的內容,本節先介紹最基本的原子變數及其背後的原理和思維。
原子變數
什麼是原子變數?為什麼需要它們呢?
在理解synchronized一節,我們介紹過一個Counter類,使用synchronized關鍵字保證原子更新操作,程式碼如下:
public class Counter {
private int count;
public synchronized void incr(){
count ++;
}
public synchronized int getCount() {
return count;
}
}
複製程式碼
對於count++這種操作來說,使用synchronzied成本太高了,需要先獲取鎖,最後還要釋放鎖,獲取不到鎖的情況下還要等待,還會有執行緒的上下文切換,這些都需要成本。
對於這種情況,完全可以使用原子變數代替,Java併發包中的基本原子變數型別有:
- AtomicBoolean:原子Boolean型別
- AtomicInteger:原子Integer型別
- AtomicLong:原子Long型別
- AtomicReference:原子引用型別
這是我們主要介紹的類,除了這四個類,還有一些其他的類,我們也會進行簡要介紹。
針對Integer, Long和Reference型別,還有對應的陣列型別:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
為了便於以原子方式更新物件中的欄位,還有如下的類:
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
AtomicReference還有兩個類似的類,在某些情況下更為易用:
- AtomicMarkableReference
- AtomicStampedReference
你可能會發現,怎麼沒有針對char, short, float, double型別的原子變數呢?大概是用的比較少吧,如果需要,可以轉換為int/long,然後使用AtomicInteger或AtomicLong。比如,對於float,可以使用如下方法和int相互轉換:
public static int floatToIntBits(float value)
public static float intBitsToFloat(int bits);
複製程式碼
下面,我們先來看幾個基本原子型別,從AtomicInteger開始。
AtomicInteger
基本用法
AtomicInteger有兩個構造方法:
public AtomicInteger(int initialValue)
public AtomicInteger()
複製程式碼
第一個構造方法給定了一個初始值,第二個的初始值為0。
可以直接獲取或設定AtomicInteger中的值,方法是:
public final int get()
public final void set(int newValue)
複製程式碼
之所以稱為原子變數,是因為其包含一些以原子方式實現組合操作的方法,比如:
//以原子方式獲取舊值並設定新值
public final int getAndSet(int newValue)
//以原子方式獲取舊值並給當前值加1
public final int getAndIncrement()
//以原子方式獲取舊值並給當前值減1
public final int getAndDecrement()
//以原子方式獲取舊值並給當前值加delta
public final int getAndAdd(int delta)
//以原子方式給當前值加1並獲取新值
public final int incrementAndGet()
//以原子方式給當前值減1並獲取新值
public final int decrementAndGet()
//以原子方式給當前值加delta並獲取新值
public final int addAndGet(int delta)
複製程式碼
這些方法的實現都依賴另一個public方法:
public final boolean compareAndSet(int expect, int update)
複製程式碼
這是一個非常重要的方法,比較並設定,我們以後將簡稱為CAS。該方法以原子方式實現瞭如下功能:如果當前值等於expect,則更新為update,否則不更新,如果更新成功,返回true,否則返回false。
AtomicInteger可以在程式中用作一個計數器,多個執行緒併發更新,也總能實現正確性,我們看個例子:
public class AtomicIntegerDemo {
private static AtomicInteger counter = new AtomicInteger(0);
static class Visitor extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
counter.incrementAndGet();
Thread.yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
int num = 100;
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
threads[i] = new Visitor();
threads[i].start();
}
for (int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter.get());
}
}
複製程式碼
程式的輸出總是正確的,為10000。
基本原理和思維
AtomicInteger的使用方法是簡單直接的,它是怎麼實現的呢?它的主要內部成員是:
private volatile int value;
複製程式碼
注意,它的宣告帶有volatile,這是必需的,以保證記憶體可見性。
它的大部分更新方法實現都類似,我們看一個方法incrementAndGet,其程式碼為:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
複製程式碼
程式碼主體是個死迴圈,先獲取當前值current,計算期望的值next,然後呼叫CAS方法進行更新,如果當前值沒有變,則更新並返回新值,否則繼續迴圈直到更新成功為止。
與synchronized鎖相比,這種原子更新方式代表一種不同的思維方式。
synchronized是悲觀的,它假定更新很可能衝突,所以先獲取鎖,得到鎖後才更新。原子變數的更新邏輯是樂觀的,它假定衝突比較少,但使用CAS更新,也就是進行衝突檢測,如果確實衝突了,那也沒關係,繼續嘗試就好了。
synchronized代表一種阻塞式演算法,得不到鎖的時候,進入鎖等待佇列,等待其他執行緒喚醒,有上下文切換開銷。原子變數的更新邏輯是非阻塞式的,更新衝突的時候,它就重試,不會阻塞,不會有上下文切換開銷。
對於大部分比較簡單的操作,無論是在低併發還是高併發情況下,這種樂觀非阻塞方式的效能都要遠高於悲觀阻塞式方式。
原子變數是比較簡單的,但對於複雜一些的資料結構和演算法,非阻塞方式往往難於實現和理解,幸運的是,Java併發包中已經提供了一些非阻塞容器,我們只需要會使用就可以了,比如:
- ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞併發佇列
- ConcurrentSkipListMap和ConcurrentSkipListSet:非阻塞併發Map和Set
這些容器我們在後續章節介紹。
但compareAndSet是怎麼實現的呢?我們看程式碼:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
複製程式碼
它呼叫了unsafe的compareAndSwapInt方法,unsafe是什麼呢?它的型別為sun.misc.Unsafe,定義為:
private static final Unsafe unsafe = Unsafe.getUnsafe();
複製程式碼
它是Sun的私有實現,從名字看,表示的也是”不安全”,一般應用程式不應該直接使用。原理上,一般的計算機系統都在硬體層次上直接支援CAS指令,而Java的實現都會利用這些特殊指令。從程式的角度看,我們可以將compareAndSet視為計算機的基本操作,直接接納就好。
實現鎖
基於CAS,除了可以實現樂觀非阻塞演算法,它也可以用來實現悲觀阻塞式演算法,比如鎖,實際上,Java併發包中的所有阻塞式工具、容器、演算法也都是基於CAS的 (不過,也需要一些別的支援)。
怎麼實現呢?我們演示一個簡單的例子,用AtomicInteger實現一個鎖MyLock,程式碼如下:
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。
AtomicBoolean/AtomicLong/AtomicReference
AtomicBoolean/AtomicLong/AtomicReference的用法和原理與AtomicInteger是類似的,我們簡要介紹下。
AtomicBoolean
AtomicBoolean可以用來在程式中表示一個標誌位,它的原子操作方法有:
public final boolean compareAndSet(boolean expect, boolean update)
public final boolean getAndSet(boolean newValue)
複製程式碼
實際上,AtomicBoolean內部使用的也是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);
}
複製程式碼
AtomicLong
AtomicLong可以用來在程式中生成唯一序列號,它的方法與AtomicInteger類似,就不贅述了。它的CAS方法呼叫的是unsafe的另一個方法,如:
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
複製程式碼
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);
}
複製程式碼
原子陣列
原子陣列方便以原子的方式更新陣列中的每個元素,我們以AtomicIntegerArray為例來簡要介紹下。
它有兩個構造方法:
public AtomicIntegerArray(int length)
public AtomicIntegerArray(int[] array)
複製程式碼
第一個會建立一個長度為length的空陣列。第二個接受一個已有的陣列,但不會直接操作該陣列,而是會建立一個新陣列,只是拷貝引數陣列中的內容到新陣列。
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)
複製程式碼
第一個引數i就是索引。看個簡單的例子:
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
複製程式碼
FieldUpdater
FieldUpdater方便以原子方式更新物件中的欄位,欄位不需要宣告為原子變數,FieldUpdater是基於反射機制實現的,我們會在後續章節介紹反射,這裡簡單介紹下其用法,看程式碼:
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());
}
}
複製程式碼
類DemoObject中有兩個成員num和ref,宣告為volatile,但不是原子變數,不過DemoObject對外提供了原子更新方法compareAndSet,它是使用欄位對應的FieldUpdater實現的,FieldUpdater是一個靜態成員,通過newUpdater工廠方法得到,newUpdater需要的引數有型別、欄位名、對於引用型別,還需要引用的具體型別。
ABA問題
使用CAS方式更新有一個ABA問題,該問題是指,一個執行緒開始看到的值是A,隨後使用CAS進行更新,它的實際期望是沒有其他執行緒修改過才更新,但普通的CAS做不到,因為可能在這個過程中,已經有其他執行緒修改過了,比如先改為了B,然後又改回為了A。
ABA是不是一個問題與程式的邏輯有關,如果是一個問題,一個解決方法是使用AtomicStampedReference,在修改值的同時附加一個時間戳,只有值和時間戳都相同才進行修改,其CAS方法宣告為:
public boolean compareAndSet(
V expectedReference, V newReference, int expectedStamp, int newStamp)
複製程式碼
比如:
Pair 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);
複製程式碼
AtomicStampedReference在compareAndSet中要同時修改兩個值,一個是引用,另一個是時間戳,它怎麼實現原子性呢?實際上,內部AtomicStampedReference會將兩個值組合為一個物件,修改的是一個值,我們看程式碼:
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)));
}
複製程式碼
這個Pair是AtomicStampedReference的一個內部類,成員包括引用和時間戳,具體定義為:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
複製程式碼
AtomicStampedReference將對引用值和時間戳的組合比較和修改轉換為了對這個內部類Pair單個值的比較和修改。
AtomicMarkableReference是另一個AtomicReference的增強類,與AtomicStampedReference類似,它也是給引用關聯了一個欄位,只是這次是一個boolean型別的標誌位,只有引用值和標誌位都相同的情況下才進行修改。
小結
本節介紹了各種原子變數的用法以及背後的原理CAS,對於併發環境中的計數、產生序列號等需求,考慮使用原子變數而非鎖,CAS是Java併發包的基礎,基於它可以實現高效的、樂觀、非阻塞式資料結構和演算法,它也是併發包中鎖、同步工具和各種容器的基礎。
下一節,我們討論併發包中的顯式鎖。
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。