Java之美[從菜鳥到高手演練]之atomic包的原理及分析,javaatomic
作者:二青
個人網站:zhangerqing.cn 郵箱:xtfggef@gmail.com 微博:http://weibo.com/xtfggef
Atomic簡介
Atomic包是java.util.concurrent下的另一個專門為安全執行緒設計的Java包,包含多個原子操作類。這個包裡面提供了一組原子變數類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的執行個體包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另一個線程進入,這隻是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,不會阻塞線程(或者說只是在硬體層級上阻塞了)。可以對基本資料、數組中的基本資料、對類中的基本資料進行操作。原子變數類相當於一種泛化的volatile變數,能夠支援原子的和有條件的讀-改-寫操作。—— 引自@chenzehe 的部落格。
傳統鎖的問題
我們先來看一個例子:計數器(Counter),採用Java裡比較方便的鎖機制synchronized關鍵字,初步的代碼如下:
class Counter {private int value;public synchronized int getValue() {return value;}public synchronized int increment() {return ++value;}public synchronized int decrement() {return --value;}}
其實像這樣的鎖機制,滿足基本的需求是沒有問題的了,但是有的時候我們的需求並非這麼簡單,我們需要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,這裡會有些問題:首先,如果被阻塞的線程優先順序很高很重要怎麼辦?其次,如果獲得鎖的線程一直不釋放鎖怎麼辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭(事實上CPU的主要工作並非這些),同時,還有可能出現一些例如死結之類的情況,最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重,因此,對於這種需求我們期待一種更合適、更高效的安全執行緒機制。
硬體同步策略
現在的處理器都支援多重處理,當然也包含多個處理器共用外圍裝置和記憶體,同時,加強了指令集以支援一些多處理的特殊需求。特別是幾乎所有的處理器都可以將其他處理器阻塞以便更新共用變數。
Compare and swap(CAS)
當前的處理器基本都支援CAS,只不過每個廠家所實現的演算法並不一樣罷了,每一個CAS操作過程都包含三個運算子:一個記憶體位址V,一個期望的值A和一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦為新值B,否則不做任何操作。CAS的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。我們來看一個例子,解釋CAS的實現過程(並非真實的CAS實現):
class SimulatedCAS {private int value;public synchronized int getValue() {return value;}public synchronized int compareAndSwap(int expectedValue, int newValue) {int oldValue = value;if (value == expectedValue)value = newValue;return oldValue;}}
下面是一個用CAS實現的Counter
public class CasCounter { private SimulatedCAS value; public int getValue() { return value.getValue(); } public int increment() { int oldValue = value.getValue(); while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue) oldValue = value.getValue(); return oldValue + 1; }}
Atomic類在JDK5.0之前,想要實現無鎖無等待的演算法是不可能的,除非用本地庫,自從有了Atomic變數類後,這成為可能。下面這張圖是java.util.concurrent.atomic包下的類結構。
- 標量類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
- 數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
- 複合變數類:AtomicMarkableReference,AtomicStampedReference
第一組AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference這四種基本類型用來處理布爾,整數,長整數,對象四種資料,其內部實現不是簡單的使用synchronized,而是一個更為高效的方式CAS (compare and swap) + volatile和native方法,從而避免了synchronized的高開銷,執行效率大為提升。我們來看個例子,與我們平時i++所對應的原子操作為:getAndIncrement()
public static void main(String[] args) {AtomicInteger ai = new AtomicInteger();System.out.println(ai);ai.getAndIncrement();System.out.println(ai);}
我們可以看一下AtomicInteger的實現:
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
這裡直接調用一個叫Unsafe的類去處理,看來我們還需要繼續看一下unsafe類的源碼了。JDK8中sun.misc下UnSafe類,點擊查看源碼
從源碼注釋得知,這個類是用於執行低層級、不安全操作的方法集合。儘管這個類和所有的方法都是公開的(public),但是這個類的使用仍然受限,你無法在自己的java程式中直接使用該類,因為只有授信的代碼才能獲得該類的執行個體。所以我們平時的代碼是無法使用這個類的,因為其設計的操作過於偏底層,如若操作不慎可能會帶來很大的災難,所以直接禁止普通代碼的訪問,當然JDK使用是沒有問題的。
Atomic中的CAS
從前面的解釋得知,CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值,此處這個“原本的一個值”怎麼來,我們看看AtomicInteger裡的實現:
// 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); } }
這裡用到UnSafe的一個方法objectFieldOffset(),查看源碼:
/** * Report the location of a given static field, in conjunction with {@link * #staticFieldBase}. * <p>Do not expect to perform any sort of arithmetic on this offset; * it is just a cookie which is passed to the unsafe heap memory accessors. * * <p>Any given field will always have the same offset, and no two distinct * fields of the same class will ever have the same offset. * * <p>As of 1.4.1, offsets for fields are represented as long values, * although the Sun JVM does not use the most significant 32 bits. * It is hard to imagine a JVM technology which needs more than * a few bits to encode an offset within a non-array object, * However, for consistency with other methods in this class, * this method reports its result as a long value. * @see #getInt(Object, long) */ public native long objectFieldOffset(Field f);
這個方法是用來拿到我們上文提到的這個“原來的值”的記憶體位址。是一個本地方法,傳回值是valueOffset。它的參數field就是AtomicInteger裡定義的value屬性:
private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; } /** * Creates a new AtomicInteger with initial value {@code 0}. */ public AtomicInteger() { }
value是一個volatile變數,在記憶體中可見,任何線程都不允許對其進行拷貝,因此JVM可以保證任何時刻任何線程總能拿到該變數的最新值。此處value的值,可以在AtomicInteger類初始化的時候傳入,也可以留空,留空則自動賦值為0。
我們再回到CAS,看看getAndIncrement()方法是怎麼利用CAS實現的。
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
繼續:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset);//------------0--------------- } while (!compareAndSwapInt(o, offset, v, v + delta));//-------------1------------- return v; }
/** * Atomically update Java variable to <tt>x</tt> if it is currently * holding <tt>expected</tt>. * @return <tt>true</tt> if successful */ public final native boolean compareAndSwapInt(Object o, long offset,//---------------2-------------- int expected, int x);
我稍微解釋一下,其實compareAndSwapInt的注釋解釋的很明確,原子的將變數的值更新為x,如果成功了返回true,我們知道,如果我們建立AtomicInteger執行個體時不傳入參數,則原始變數的值即為0,所以上面//----------0-----------處得到的v的值即為0,1處的代碼為:
while(!compareAndSwapInt(o, offset, 0, 1))我們知道offset指向的地址對應的值就是原始變數的初值0,所以與期望的值0相同,所以將初值賦值為1,返回true,取反後為false,迴圈結束,返回v即更新之前的值0. 這就是類似於i++操作的原子操作的實現,當然最終CAS的實現都是native的,用C語言實現的,我們這裡看不到源碼,有時間我會反編譯一下這段代碼看看。
CAS安全執行緒
說了半天,我們要迴歸到最原始的問題了:這樣怎麼實現安全執行緒呢?請大家自己先考慮一下這個問題,其實我們在語言層面是沒有做任何同步的操作的,大家也可以看到源碼沒有任何鎖加在上面,可它為什麼是安全執行緒的呢?這就是Atomic包下這些類的奧秘:語言層面不做處理,我們將其交給硬體—CPU和記憶體,利用CPU的多處理能力,實現硬體層面的阻塞,再加上volatile變數的特性即可實現基於原子操作的安全執行緒。所以說,CAS並不是無阻塞,只是阻塞並非在語言、線程方面,而是在硬體層面,所以無疑這樣的操作會更快更高效!
總結
雖然基於CAS的安全執行緒機制很好很高效,但要說的是,並非所有安全執行緒都可以用這樣的方法來實現,這隻適合一些粒度比較小,型如計數器這樣的需求用起來才有效,否則也不會有鎖的存在了。
有問題請隨時聯絡我,我會及時回複,歡迎一起探討!
作者:二青 個人網站:zhangerqing.cn 郵箱:xtfggef@gmail.com 微博:http://weibo.com/xtfggef