atomic包的原理及分析



Atomic簡介

Atomic包是Java.util.concurrent下的另一個專門爲線程安全設計的Java包,包含多個原子操作類。這個包裏面提供了一組原子變量類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另一個線程進入,這只是一種邏輯上的理解。實際上是藉助硬件的相關指令來實現的,不會阻塞線程(或者說只是在硬件級別上阻塞了)。可以對基本數據、數組中的基本數據、對類中的基本數據進行操作。原子變量類相當於一種泛化的volatile變量,能夠支持原子的和有條件的讀-改-寫操作。——  引自@chenzehe 的博客。


傳統鎖的問題

我們先來看一個例子:計數器(Counter),採用Java裏比較方便的鎖機制synchronized關鍵字,初步的代碼如下:

[java] view plain copy
  1. class Counter {  
  2.           
  3.     private int value;  
  4.   
  5.     public synchronized int getValue() {  
  6.         return value;  
  7.     }  
  8.   
  9.     public synchronized int increment() {  
  10.         return ++value;  
  11.     }  
  12.   
  13.     public synchronized int decrement() {  
  14.         return --value;  
  15.     }  
  16. }   

其實像這樣的鎖機制,滿足基本的需求是沒有問題的了,但是有的時候我們的需求並非這麼簡單,我們需要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,這裏會有些問題:首先,如果被阻塞的線程優先級很高很重要怎麼辦?其次,如果獲得鎖的線程一直不釋放鎖怎麼辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭(事實上CPU的主要工作並非這些),同時,還有可能出現一些例如死鎖之類的情況,最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重,因此,對於這種需求我們期待一種更合適、更高效的線程安全機制。


硬件同步策略

現在的處理器都支持多重處理,當然也包含多個處理器共享外圍設備和內存,同時,加強了指令集以支持一些多處理的特殊需求。特別是幾乎所有的處理器都可以將其他處理器阻塞以便更新共享變量。


Compare and swap(CAS)

當前的處理器基本都支持CAS,只不過每個廠家所實現的算法並不一樣罷了,每一個CAS操作過程都包含三個運算符:一個內存地址V,一個期望的值A和一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦爲新值B,否則不做任何操作。CAS的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。我們來看一個例子,解釋CAS的實現過程(並非真實的CAS實現):

[java] view plain copy
  1. class SimulatedCAS {  
  2.     private int value;  
  3.   
  4.     public synchronized int getValue() {  
  5.         return value;  
  6.     }  
  7.     public synchronized int compareAndSwap(int expectedValue, int newValue) {  
  8.         int oldValue = value;  
  9.         if (value == expectedValue)  
  10.             value = newValue;  
  11.         return oldValue;  
  12.     }  
  13. }  

下面是一個用CAS實現的Counter

[java] view plain copy
  1. public class CasCounter {  
  2.     private SimulatedCAS value;  
  3.   
  4.     public int getValue() {  
  5.         return value.getValue();  
  6.     }  
  7.   
  8.     public int increment() {  
  9.         int oldValue = value.getValue();  
  10.         while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue)  
  11.             oldValue = value.getValue();  
  12.         return oldValue + 1;  
  13.     }  
  14. }  

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()

[java] view plain copy
  1. public static void main(String[] args) {  
  2.     AtomicInteger ai = new AtomicInteger();  
  3.     System.out.println(ai);  
  4.     ai.getAndIncrement();  
  5.     System.out.println(ai);  
  6. }  

我們可以看一下AtomicInteger的實現:

[java] view plain copy
  1. /** 
  2.  * Atomically increments by one the current value. 
  3.  * 
  4.  * @return the previous value 
  5.  */  
  6. public final int getAndIncrement() {  
  7.     return unsafe.getAndAddInt(this, valueOffset, 1);  
  8. }  

這裏直接調用一個叫Unsafe的類去處理,看來我們還需要繼續看一下unsafe類的源碼了。JDK8中sun.misc下UnSafe類,點擊查看源碼

從源碼註釋得知,這個類是用於執行低級別、不安全操作的方法集合。儘管這個類和所有的方法都是公開的(public),但是這個類的使用仍然受限,你無法在自己的java程序中直接使用該類,因爲只有授信的代碼才能獲得該類的實例。所以我們平時的代碼是無法使用這個類的,因爲其設計的操作過於偏底層,如若操作不慎可能會帶來很大的災難,所以直接禁止普通代碼的訪問,當然JDK使用是沒有問題的。


Atomic中的CAS

從前面的解釋得知,CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值,此處這個“原本的一個值”怎麼來,我們看看AtomicInteger裏的實現:

[java] view plain copy
  1. // setup to use Unsafe.compareAndSwapInt for updates  
  2.     private static final Unsafe unsafe = Unsafe.getUnsafe();  
  3.     private static final long valueOffset;  
  4.   
  5.     static {  
  6.         try {  
  7.             valueOffset = unsafe.objectFieldOffset  
  8.                 (AtomicInteger.class.getDeclaredField("value"));  
  9.         } catch (Exception ex) { throw new Error(ex); }  
  10.     }  

這裏用到UnSafe的一個方法objectFieldOffset(),查看源碼:

[java] view plain copy
  1. /** 
  2.      * Report the location of a given static field, in conjunction with {@link 
  3.      * #staticFieldBase}. 
  4.      * <p>Do not expect to perform any sort of arithmetic on this offset; 
  5.      * it is just a cookie which is passed to the unsafe heap memory accessors. 
  6.      * 
  7.      * <p>Any given field will always have the same offset, and no two distinct 
  8.      * fields of the same class will ever have the same offset. 
  9.      * 
  10.      * <p>As of 1.4.1, offsets for fields are represented as long values, 
  11.      * although the Sun JVM does not use the most significant 32 bits. 
  12.      * It is hard to imagine a JVM technology which needs more than 
  13.      * a few bits to encode an offset within a non-array object, 
  14.      * However, for consistency with other methods in this class, 
  15.      * this method reports its result as a long value. 
  16.      * @see #getInt(Object, long) 
  17.      */  
  18.     public native long objectFieldOffset(Field f);  

這個方法是用來拿到我們上文提到的這個“原來的值”的內存地址。是一個本地方法,返回值是valueOffset。它的參數field就是AtomicInteger裏定義的value屬性:

[java] view plain copy
  1. private volatile int value;  
  2.   
  3. /** 
  4.  * Creates a new AtomicInteger with the given initial value. 
  5.  * 
  6.  * @param initialValue the initial value 
  7.  */  
  8. public AtomicInteger(int initialValue) {  
  9.     value = initialValue;  
  10. }  
  11.   
  12. /** 
  13.  * Creates a new AtomicInteger with initial value {@code 0}. 
  14.  */  
  15. public AtomicInteger() {  
  16. }  

value是一個volatile變量,在內存中可見,任何線程都不允許對其進行拷貝,因此JVM可以保證任何時刻任何線程總能拿到該變量的最新值。此處value的值,可以在AtomicInteger類初始化的時候傳入,也可以留空,留空則自動賦值爲0。


我們再回到CAS,看看getAndIncrement()方法是怎麼利用CAS實現的。

[java] view plain copy
  1. /** 
  2.     * Atomically increments by one the current value. 
  3.     * 
  4.     * @return the previous value 
  5.     */  
  6.    public final int getAndIncrement() {  
  7.        return unsafe.getAndAddInt(this, valueOffset, 1);  
  8.    }  

繼續:

[java] view plain copy
  1. public final int getAndAddInt(Object o, long offset, int delta) {  
  2.         int v;  
  3.         do {  
  4.             v = getIntVolatile(o, offset);//------------0---------------  
  5.         } while (!compareAndSwapInt(o, offset, v, v + delta));//-------------1-------------  
  6.         return v;  
  7.     }  

[java] view plain copy
  1. /** 
  2.    * Atomically update Java variable to <tt>x</tt> if it is currently 
  3.    * holding <tt>expected</tt>. 
  4.    * @return <tt>true</tt> if successful 
  5.    */  
  6.   public final native boolean compareAndSwapInt(Object o, long offset,//---------------2--------------  
  7.                                                 int expected,  
  8.                                                 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的線程安全機制很好很高效,但要說的是,並非所有線程安全都可以用這樣的方法來實現,這隻適合一些粒度比較小,型如計數器這樣的需求用起來纔有效,否則也不會有鎖的存在了。

發佈了20 篇原創文章 · 獲贊 14 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章