本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml
從本節開始,咱們探討Java併發工具包java.util.concurrent中的內容,本節先介紹最基本的原子變量及其背後的原理和思惟。java
原子變量git
什麼是原子變量?爲何須要它們呢?github
在理解synchronized一節,咱們介紹過一個Counter類,使用synchronized關鍵字保證原子更新操做,代碼以下:算法
public class Counter { private int count; public synchronized void incr(){ count ++; } public synchronized int getCount() { return count; } }
對於count++這種操做來講,使用synchronzied成本過高了,須要先獲取鎖,最後還要釋放鎖,獲取不到鎖的狀況下還要等待,還會有線程的上下文切換,這些都須要成本。編程
對於這種狀況,徹底可使用原子變量代替,Java併發包中的基本原子變量類型有:swift
這是咱們主要介紹的類,除了這四個類,還有一些其餘的類,咱們也會進行簡要介紹。數組
針對Integer, Long和Reference類型,還有對應的數組類型:安全
爲了便於以原子方式更新對象中的字段,還有以下的類:微信
AtomicReference還有兩個相似的類,在某些狀況下更爲易用:
你可能會發現,怎麼沒有針對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併發包中已經提供了一些非阻塞容器,咱們只須要會使用就能夠了,好比:
這些容器咱們在後續章節介紹。
但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編程及計算機技術的本質。用心原創,保留全部版權。