本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營鏈接:http://item.jd.com/12299018.html
從本節開始,我們探討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並發包的基礎,基於它可以實現高效的、樂觀、非阻塞式數據結構和算法,它也是並發包中鎖、同步工具和各種容器的基礎。
下一節,我們討論並發包中的顯式鎖。
(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic)
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。