1. 程式人生 > >Java JUC之Atomic系列12大類例項講解和原理分解

Java JUC之Atomic系列12大類例項講解和原理分解

在java6以後我們不但接觸到了Lock相關的鎖,也接觸到了很多更加樂觀的原子修改操作,也就是在修改時我們只需要保證它的那個瞬間是安全的即可,經過相應的包裝後可以再處理物件的併發修改,以及併發中的ABA問題,本文講述Atomic系列的類的實現以及使用方法,其中包含:

基本類:AtomicInteger、AtomicLong、AtomicBoolean;

引用型別:AtomicReference、AtomicReference的ABA例項、AtomicStampedRerence、AtomicMarkableReference;

陣列型別:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

屬性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

看到這麼多類,你是否覺得很困惑,其實沒什麼,因為你只需要看懂一個,其餘的方法和使用都是大同小異的,相關的類會介紹他們之間的區別在哪裡,在使用中需要注意的地方即可。

在使用Atomic系列前,我們需要先知道一個東西就是Unsafe類,全名為:sun.misc.Unsafe,這個類包含了大量的對C程式碼的操作,包括很多直接記憶體分配以及原子操作的呼叫,而它之所以標記為非安全的,是告訴你這個裡面大量的方法呼叫都會存在安全隱患,需要小心使用,否則會導致嚴重的後果,例如在通過unsafe分配記憶體的時候,如果自己指定某些區域可能會導致一些類似C++一樣的指標越界到其他程序的問題,不過它的具體使用並不是本文的重點,本文重點是Atomic系列的內容大多會基於unsafe類中的以下幾個本地方法來操作:

物件的引用進行對比後交換,交換成功返回true,交換失敗返回false,這個交換過程完全是原子的,在CPU上計算完結果後,都會對比記憶體的結果是否還是原先的值,若不是,則認為不能替換,因為變數是volatile型別所以最終寫入的資料會被其他執行緒看到,所以一個執行緒修改成功後,其他執行緒就發現自己修改失敗了。

引數1:物件所在的類本身的物件(一般這裡是對一個物件的屬性做修改,才會出現併發,所以該物件所存在的類也是有一個物件的)

引數2:這個屬性在這個物件裡面的相對便宜量位置,其實對比時是對比記憶體單元,所以需要屬性的起始位置,而引用就是修改引用地址(根據OS、VM位數和引數配置決定寬度一般是4-8個位元組),int就是修改相關的4個位元組,而long就是修改相關的8個位元組。

獲取偏移量也是通過unsafe的一個方法:objectFieldOffset(Fieldfield)來獲取屬性在物件中的偏移量;靜態變數需要通過:staticFieldOffset(Field field)獲取,呼叫的總方法是:fieldOffset(Fieldfield)

引數3:修改的引用的原始值,用於對比原來的引用和要修改的目標是否一致。

引數4:修改的目標值,要將資料修改成什麼。

  1. publicfinalnativeboolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);  
  2. publicfinalnativeboolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);  

#對long的操作,要看VM是否支援對Long的CAS,因為有可能VM本身不支援,若不支援,此時運算會變成Lock方式,不過現在VM都基本是支援的而已。

  1. publicfinalnativeboolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);  

我們不推薦直接使用unsafe來操作原子變數,而是通過java封裝好的一些類來操作原子變數。

例項程式碼1:AtomicIntegerTest.java

  1. import java.util.concurrent.atomic.AtomicInteger;  
  2. publicclass AtomicIntegerTest {  
  3.     /** 
  4.      * 常見的方法列表 
  5.      * @see AtomicInteger#get()             直接返回值 
  6.      * @see AtomicInteger#getAndAdd(int)    增加指定的資料,返回變化前的資料 
  7.      * @see AtomicInteger#getAndDecrement() 減少1,返回減少前的資料 
  8.      * @see AtomicInteger#getAndIncrement() 增加1,返回增加前的資料 
  9.      * @see AtomicInteger#getAndSet(int)    設定指定的資料,返回設定前的資料 
  10.      *  
  11.      * @see AtomicInteger#addAndGet(int)    增加指定的資料後返回增加後的資料 
  12.      * @see AtomicInteger#decrementAndGet() 減少1,返回減少後的值 
  13.      * @see AtomicInteger#incrementAndGet() 增加1,返回增加後的值 
  14.      * @see AtomicInteger#lazySet(int)      僅僅當get時才會set 
  15.      *  
  16.      * @see AtomicInteger#compareAndSet(int, int) 嘗試新增後對比,若增加成功則返回true否則返回false 
  17.      */
  18.     publicfinalstatic AtomicInteger TEST_INTEGER = new AtomicInteger(1);  
  19.     publicstaticvoid main(String []args) throws InterruptedException {  
  20.         final Thread []threads = new Thread[10];  
  21.          for(int i = 0 ; i < 10 ; i++) {  
  22.              finalint num = i;  
  23.              threads[i] = new Thread() {  
  24.                  publicvoid run() {  
  25.                      try {  
  26.                         Thread.sleep(1000);  
  27.                     } catch (InterruptedException e) {  
  28.                         e.printStackTrace();  
  29.                     }  
  30.                     int now = TEST_INTEGER.incrementAndGet();  
  31.                     System.out.println("我是執行緒:" + num + ",我得到值了,增加後的值為:" + now);  
  32.                  }  
  33.              };  
  34.              threads[i].start();  
  35.          }  
  36.          for(Thread t : threads) {  
  37.              t.join();  
  38.          }  
  39.          System.out.println("最終執行結果:" + TEST_INTEGER.get());  
  40.     }  
  41. }<strong>  
  42. </strong>  

程式碼例子中模擬多個執行緒併發對AtomicInteger進行增加1的操作,如果這個資料是普通型別,那麼增加過程中出現的問題就是兩個執行緒可能同時看到的資料都是同一個資料,增加完成後寫回的時候,也是同一個資料,但是兩個加法應當序列增加1,也就是加2的操作,甚至於更加特殊的情況是一個執行緒加到3後,寫入,另一個執行緒寫入了2,還越變越少,也就是不能得到正確的結果,在併發下,我們模擬計數器,要得到精確的計數器值,就需要使用它,我們希望得到的結果是11,可以拷貝程式碼進去執行後看到結果的確是11,順然輸出的順序可能不一樣,也同時可以證明執行緒的確是併發執行的(只是在輸出的時候,徵用System.out這個物件也不一定是誰先搶到),但是最終結果的確是11。

相信你對AtomicInteger的使用有一些瞭解了吧,要知道更多的方法使用,請參看這段程式碼中定義變數位置的註釋,有關於AtomicInteger的相關方法的詳細註釋,可以直接跟蹤進去看原始碼,註釋中使用了簡單的描述說明了方法的用途。

而對於AtomicLong呢,其實和AtomicInteger差不多,唯一的區別就是它處理的資料是long型別的就是了;

對於AtomicBoolean呢,方法要少一些,常見的方法就兩個:

  1. AtomicBoolean#compareAndSet(booleanboolean)  第一個引數為原始值,第二個引數為要修改的新值,若修改成功則返回true,否則返回false
  2. AtomicBoolean#getAndSet(boolean)   嘗試設定新的boolean值,直到成功為止,返回設定前的資料  

因為boolean值就兩個值,所以就是來回改,相對的很多增加減少的方法自然就沒有了,對於使用來講,我們列舉一個boolean的併發修改,僅有一個執行緒可以修改成功的例子:

例項程式碼2:AtomicBooleanTest.java

  1. import java.util.concurrent.atomic.AtomicBoolean;  
  2. publicclass AtomicBooleanTest {  
  3.     /** 
  4.      * 主要方法: 
  5.      * @see AtomicBoolean#compareAndSet(boolean, boolean)  第一個引數為原始值,第二個引數為要修改的新值,若修改成功則返回true,否則返回false 
  6.      * @see AtomicBoolean#getAndSet(boolean)   嘗試設定新的boolean值,直到成功為止,返回設定前的資料 
  7.      */
  8.     publicfinalstatic AtomicBoolean TEST_BOOLEAN = new AtomicBoolean();  
  9.     publicstaticvoid main(String []args) {  
  10.         for(int i = 0 ; i < 10 ; i++) {  
  11.             new Thread() {  
  12.                 publicvoid run() {  
  13.                     try {  
  14.                         Thread.sleep(1000);  
  15.                     } catch (InterruptedException e) {  
  16.                         e.printStackTrace();  
  17.                     }  
  18.                     if(TEST_BOOLEAN.compareAndSet(falsetrue)) {  
  19.                         System.out.println("我成功了!");  
  20.                     }  
  21.                 }  
  22.             }.start();  
  23.         }  
  24.     }  
  25. }  

這裡有10個執行緒,我們讓他們幾乎同時去徵用boolean值的修改,修改成功者輸出:我成功了!此時你執行完你會發現只會輸出一個“我成功了!”,說明徵用過程中達到了鎖的效果。

那麼幾種基本型別就說完了,我們來看看裡面的實現是不是如我們開始說的Unsafe那樣,看幾段原始碼即可,我們看下AtomicInteger的一些原始碼,例如開始用的:incrementAndGet方法,這個,它的原始碼是:

  1. publicfinalint incrementAndGet() {  
  2.     for (;;) {  
  3.         int current = get();  
  4.         int next = current + 1;  
  5.         if (compareAndSet(current, next))  
  6.             return next;  
  7.     }  
  8. }  

可以看到內部有一個死迴圈,只有不斷去做compareAndSet操作,直到成功為止,也就是修改的根本在compareAndSet方法裡面,可以去看下相關的修改方法均是這樣實現,那麼看下compareAndSet方法的body部分是:

  1. publicfinalboolean compareAndSet(int expect, int update) {  
  2.     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
  3. }  

可以看到這裡使用了unsafe的compareAndSwapInt的方法,很明顯this就是指AtomicInteger當前的這個物件(這個物件不用像上面說的它不能是static和final,它無所謂的),而valueOffset的定義是這樣的:

  1. privatestaticfinallong valueOffset;  
  2.     static {  
  3.       try {  
  4.         valueOffset = unsafe.objectFieldOffset  
  5.             (AtomicInteger.class.getDeclaredField("value"));  
  6.       } catch (Exception ex) {   
  7.          thrownew Error(ex); }  
  8. }  

可以看出是通過我們前面所述的objectFieldOffset方法來獲取的屬性偏移量,所以你自己如果定義類似的操作的時候,就要注意,這個屬性不能是靜態的,否則不能用這個方法來獲取。

後面兩個引數自然是對比值和需要修改的目標物件的地址。

其實Atomic系列你看到這裡,java層面你就知道差不多了,其餘的就是特殊用法和包裝而已,剛才我們說了unsafe的3個方法無非是地址和值的區別在記憶體層面是沒有本質區別的,因為地址本身也是數字值。

為了說明這個問題,我們就先說Reference的使用:

我們測試一個reference,和boolean測試方式一樣,也是測試多個執行緒只有一個執行緒能修改它。

例項程式碼1:AtomicReferenceTest.java

  1. import java.util.concurrent.atomic.AtomicReference;  
  2. publicclass AtomicReferenceTest {  
  3.     /** 
  4.      * 相關方法列表 
  5.      * @see AtomicReference#compareAndSet(Object, Object) 對比設定值,引數1:原始值,引數2:修改目標引用 
  6.      * @see AtomicReference#getAndSet(Object) 將引用的目標修改為設定的引數,直到修改成功為止,返回修改前的引用 
  7.      */
  8.     publicfinalstatic AtomicReference <String>ATOMIC_REFERENCE = new AtomicReference<String>("abc");  
  9.     publicstaticvoid main(String []args) {  
  10.         for(int i = 0 ; i < 100 ; i++) {  
  11.             finalint num = i;  
  12.             new Thread() {  
  13.                 publicvoid run() {  
  14.                     try {  
  15.                         Thread.sleep(Math.abs((int)(Math.random() * 100)));  
  16.                     } catch (InterruptedException e) {  
  17.                         e.printStackTrace();  
  18.                     }  
  19.                     if(ATOMIC_REFERENCE.compareAndSet("abc"new String("abc"))) {  
  20.                         System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");  
  21.                     }  
  22.                 }  
  23.             }.start();  
  24.         }  
  25.     }  
  26. }  

測試結果如我們所料,的確只有一個執行緒,執行,跟著程式碼:compareAndSet進去,發現原始碼中的呼叫是:

  1. publicfinalboolean compareAndSet(V expect, V update) {  
  2.     return unsafe.compareAndSwapObject(this, valueOffset, expect, update);  
  3. }  

OK,的確和我們上面所講一致,那麼此時我們又遇到了引用修改的新問題,什麼問題呢?ABA問題,什麼是ABA問題呢,當某些流程在處理過程中是順向的,也就是不允許重複處理的情況下,在某些情況下導致一個數據由A變成B,再中間可能經過0-N個環節後變成了A,此時A不允許再變成B了,因為此時的狀態已經發生了改變,例如:銀行資金裡面做一批賬目操作,要求資金在80-100元的人,增加20元錢,時間持續一天,也就是後臺程式會不斷掃描這些使用者的資金是否是在這個範圍,但是要求增加過的人就不能再增加了,如果增加20後,被人取出10元繼續在這個範圍,那麼就可以無限套現出來,就是ABA問題了,類似的還有搶紅包或中獎,比如每天每個人限量3個紅包,中那個等級的獎的個數等等。

此時我們需要使用的方式就不是簡單的compareAndSet操作,因為它僅僅是考慮到物理上的併發,而不是在業務邏輯上去控制順序,此時我們需要借鑑資料庫的事務序列號的一些思想來解決,假如每個物件修改的次數可以記住,修改前先對比下次數是否一致再修改,那麼這個問題就簡單了,AtomicStampedReference類正是提供這一功能的,其實它僅僅是在AtomicReference類的再一次包裝,裡面增加了一層引用和計數器,其實是否為計數器完全由自己控制,大多數我們是讓他自增的,你也可以按照自己的方式來標示版本號,下面一個例子是ABA問題的簡單演示:

例項程式碼3(ABA問題模擬程式碼演示):
  1. import java.util.concurrent.atomic.AtomicReference;  
  2. /** 
  3.  * ABA問題模擬,執行緒併發中,導致ABA問題,解決方案是使用|AtomicMarkableReference 
  4.  * 請參看相應的例子:AtomicStampedReferenceTest、AtomicMarkableReferenceTest 
  5.  * @author zhongyin.xy 
  6.  * 
  7.  */
  8. publicclass AtomicReferenceABATest {  
  9.     publicfinalstatic AtomicReference <String>ATOMIC_REFERENCE = new AtomicReference<String>("abc");  
  10.     publicstaticvoid main(String []args) {  
  11.         for(int i = 0 ; i < 100 ; i++) {  
  12.             finalint num = i;  
  13.             new Thread() {  
  14.                 publicvoid run() {  
  15.                     try {  
  16.                         Thread.sleep(Math.abs((int)(Math.random() * 100)));  
  17.                     } catch (InterruptedException e) {  
  18.                         e.printStackTrace();  
  19.                     }  
  20.                     if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2")) {  
  21.                         System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");  
  22.                     }  
  23.                 }  
  24.             }.start();  
  25.         }  
  26.         new Thread() {  
  27.             publicvoid run() {  
  28.                 while(!ATOMIC_REFERENCE.compareAndSet("abc2""abc"));  
  29.                 System.out.println("已經改為原始值!");  
  30.             }  
  31.         }.start();  
  32.     }  
  33. }<strong>  
  34. </strong>  

程式碼中和原來的例子,唯一的區別就是最後增加了一個執行緒讓他將資料修改為原來的值,並一直嘗試修改,直到修改成功為止,為什麼沒有直接用:方法呢getAndSet方法呢,因為我們的目的是要讓某個執行緒先將他修改為abc2後再讓他修改回abc,所以需要這樣做;

此時我們得到的結果是:

我是執行緒:41,我獲得了鎖進行了物件修改!

已經改為原始值!

我是執行緒:85,我獲得了鎖進行了物件修改!

當然你的執行緒編號多半和我不一樣,只要徵用到就對,可以發現,有兩個執行緒修改了這個字串,我們是想那一堆將abc改成abc2的執行緒僅有一個成功,即使其他執行緒在他們徵用時將其修改為abc,也不能再修改。

此時我們通過類來AtomicStampedReference解決這個問題:

例項程式碼4(AtomicStampedReference解決ABA問題):

  1. import java.util.concurrent.atomic.AtomicStampedReference;  
  2. publicclass AtomicStampedReferenceTest {  
  3.     publicfinalstatic AtomicStampedReference <String>ATOMIC_REFERENCE = new AtomicStampedReference<String>("abc" , 0);  
  4.     publicstaticvoid main(String []args) {  
  5.         for(int i = 0 ; i < 100 ; i++) {  
  6.             finalint num = i;  
  7.             finalint stamp = ATOMIC_REFERENCE.getStamp();  
  8.             new Thread() {  
  9.                 publicvoid run() {  
  10.                     try {  
  11.                         Thread.sleep(Math.abs((int)(Math.random() * 100)));  
  12.                     } catch (InterruptedException e) {  
  13.                         e.printStackTrace();  
  14.                     }  
  15.                     if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2" , stamp , stamp + 1)) {  
  16.                         System.out.println("我是執行緒:" + num + ",我獲得了鎖進行了物件修改!");  
  17.                     }  
  18.                 }  
  19.             }.start();  
  20.         }  
  21.         new Thread() {  
  22.             publicvoid run() {  
  23.                 int stamp = ATOMIC_REFERENCE.getStamp();  
  24.                 while(!ATOMIC_REFERENCE.compareAndSet("abc2""abc" , stamp , stamp + 1));  
  25.                 System.out.println("已經改回為原始值!");  
  26.             }  
  27.         }.start();  
  28.     }  
  29. }  


此時再執行程式看到的結果就是我們想要的了,發現將abc修改為abc2的執行緒僅有一個被訪問,雖然被修改回了原始值,但是其他執行緒也不會再將abc改為abc2。

而類:AtomicMarkableReferenceAtomicStampedReference功能差不多,有點區別的是:它描述更加簡單的是與否的關係,通常ABA問題只有兩種狀態,而AtomicStampedReference是多種狀態,那麼為什麼還要有AtomicMarkableReference呢,因為它在處理是與否上面更加具有可讀性,而AtomicStampedReference過於隨意定義狀態,並不便於閱讀大量的是和否的關係,它可以被認為是一個計數器或狀態列表等資訊,java提倡通過類名知道其意義,所以這個類的存在也是必要的,它的定義就是將資料變換為true|false如下:

  1. publicfinalstatic AtomicMarkableReference <String>ATOMIC_MARKABLE_REFERENCE = new AtomicMarkableReference<String>("abc" , false);  

操作時使用:

  1. ATOMIC_MARKABLE_REFERENCE.compareAndSet("abc""abc2"falsetrue);  

好了,reference的三個類的種類都介紹了,我們下面要開始說Atomic的陣列用法,因為我們開始說到的都是一些簡單變數和基本資料,運算元組呢?如果你來設計會怎麼設計,Atomic的陣列要求不允許修改長度等,不像集合類那麼豐富的操作,不過它可以讓你的陣列上每個元素的操作絕對安全的,也就是它細化的力度還是到陣列上的元素,為你做了二次包裝,所以如果你來設計,就是在原有的操作上增加一個下標訪問即可,我們來模擬一個Integer型別的陣列,即:AtomicIntegerArray

例項程式碼5(AtomicIntegerArrayTest.java)

  1. import java.util.concurrent.atomic.AtomicIntegerArray;  
  2. publicclass AtomicIntegerArrayTest {  
  3.     /** 
  4.      * 常見的方法列表 
  5.      * @see AtomicIntegerArray#addAndGet(int, int) 執行加法,第一個引數為陣列的下標,第二個引數為增加的數量,返回增加後的結果 
  6.      * @see AtomicIntegerArray#compareAndSet(int, int, int) 對比修改,引數1:陣列下標,引數2:原始值,引數3,修改目標值,修改成功返回true否則false 
  7.      * @see AtomicIntegerArray#decrementAndGet(int) 引數為陣列下標,將陣列對應數字減少1,返回減少後的資料 
  8.      * @see AtomicIntegerArray#incrementAndGet(int) 引數為陣列下標,將陣列對應數字增加1,返回增加後的資料 
  9.      *  
  10.      * @see AtomicIntegerArray#getAndAdd(int, int) 和addAndGet類似,區別是返回值是變化前的資料 
  11.      * @see AtomicIntegerArray#getAndDecrement(int) 和decrementAndGet類似,區別是返回變化前的資料 
  12.      * @see AtomicIntegerArray#getAndIncrement(int) 和incrementAndGet類似,區別是返回變化前的資料 
  13.      * @see AtomicIntegerArray#getAndSet(int, int) 將對應下標的數字設定為指定值,第二個引數為設定的值,返回是變化前的資料 
  14.      */
  15.     privatefinalstatic AtomicIntegerArray ATOMIC_INTEGER_ARRAY = new AtomicIntegerArray(newint[]{1,2,3,4,5,6,7,8,9,10});  
  16.     publicstaticvoid main(String []args) throws InterruptedException {  
  17.         Thread []threads = new Thread[100];  
  18.         for(int i = 0 ; i < 100 ; i++) {  
  19.             finalint index = i % 10;  
  20.             finalint threadNum = i;  
  21.             threads[i] = new Thread() {  
  22.                 publicvoid run() {  
  23.                     int result = ATOMIC_INTEGER_ARRAY.addAndGet(index, index + 1);  
  24.                     System.out.println("執行緒編號為:" + threadNum + " , 對應的原始值為:" + (index + 1) + ",增加後的結果為:" + result);  
  25.                 }  
  26.             };  
  27.             threads[i].start();  
  28.         }  
  29.         for(Thread thread : threads) {  
  30.             thread.join();  
  31.         }  
  32.         System.out.println("=========================>\n執行已經完成,結果列表:");  
  33.         for(int i = 0 ; i < ATOMIC_INTEGER_ARRAY.length() ; i++) {  
  34.             System.out.println(ATOMIC_INTEGER_ARRAY.get(i));  
  35.         }  
  36.     }  
  37. }  

計算結果說明:100個執行緒併發,每10個執行緒會被併發修改陣列中的一個元素,也就是陣列中的每個元素會被10個執行緒併發修改訪問,每次增加原始值的大小,此時運算完的結果看最後輸出的敲好為原始值的11倍數,和我們預期的一致,如果不是執行緒安全那麼這個值什麼都有可能。

而相應的類:AtomicLongArray其實和AtomicIntegerArray操作方法類似,最大區別就是它操作的資料型別是long;而AtomicRerenceArray也是這樣,只是它方法只有兩個:


  1. AtomicReferenceArray#compareAndSet(int, Object, Object)   
  2. 引數1:陣列下標;  
  3. 引數2:修改原始值對比;  
  4. 引數3:修改目標值   
  5. 修改成功返回true,否則返回false
  6. AtomicReferenceArray#getAndSet(int, Object)   
  7. 引數1:陣列下標  
  8. 引數2:修改的目標  
  9. 修改成功為止,返回修改前的資料  

到這裡你是否對陣列內部的操作應該有所瞭解了,和當初預期一樣,引數就是多了一個下標,為了完全驗證這點,跟蹤到原始碼中可以看到:

  1. publicfinalint addAndGet(int i, int delta) {  
  2.         while (true) {  
  3.             int current = get(i);  
  4.             int next = current + delta;  
  5.             if (compareAndSet(i, current, next))  
  6.                 return next;  
  7.         }  
  8.     }  

可以看到根據get(i)獲取到對應的資料,然後做和普通AtomicInteger差不多的操作,get操作裡面有個細節是:

  1. publicfinalint get(int i) {  
  2.     return unsafe.getIntVolatile(array, rawIndex(i));  
  3. }  

這裡通過了unsafe獲取基於volatile方式獲取(可見性)獲取一個int型別的資料,而獲取的位置是由rawIndex來確定,它的原始碼是:

  1. privatelong rawIndex(int i) {  
  2.     if (i < 0 || i >= array.length)  
  3.         thrownew IndexOutOfBoundsException("index " + i);  
  4.     return base + (long) i * scale;  
  5. }  

可以發現這個結果是一個地址位置,為base加上一耳光偏移量,那麼看看base和scale的定義為:

  1. privatestaticfinalint base = unsafe.arrayBaseOffset(int[].class);  
  2. privatestaticfinalint scale = unsafe.arrayIndexScale(int[].class);  

可以發現unsafe裡面提供了對陣列base的位置的獲取,因為物件是有頭部的,而陣列還有一個長度位置,第二個很明顯是一個數組元素所佔用的寬度,也就是基本精度;這裡應該可以體會到unsafe所帶來的強大了吧。

本文最後要介紹的部分為Updater也就是修改器,它算是Atomic的系列的一個擴充套件,Atomic系列是為你定義好的一些物件,你可以使用,但是如果是別人已經在使用的物件會原先的程式碼需要修改為Atomic系列,此時若全部修改型別到對應的物件相信很麻煩,因為牽涉的程式碼會很多,此時java提供一個外部的Updater可以對物件的屬性本身的修改提供類似Atomic的操作,也就是它對這些普通的屬性的操作是併發下安全的,分別由:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceUpdater,這樣操作後,系統會更加靈活,也就是可能那些類的屬性只是在某些情況下需要控制併發,很多時候不需要,但是他們的使用通常有以下幾個限制:

限制1:操作的目標不能是static型別,前面說到unsafe的已經可以猜測到它提取的是非static型別的屬性偏移量,如果是static型別在獲取時如果沒有使用對應的方法是會報錯的,而這個Updater並沒有使用對應的方法。

限制2:操作的目標不能是final型別的,因為final根本沒法修改。

限制3:必須是volatile型別的資料,也就是資料本身是讀一致的。

限制4:屬性必須對當前的Updater所在的區域是可見的,也就是private如果不是當前類肯定是不可見的,protected如果不存在父子關係也是不可見的,default如果不是在同一個package下也是不可見的。

實現方式:通過反射找到屬性,對屬性進行操作,但是並不是設定accessable,所以必須是可見的屬性才能操作。

說了這麼多,來個例項看看吧。

例項程式碼6:(AtomicIntegerFieldUpdaterTest.java)

  1. import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;  
  2. publicclass AtomicIntegerFieldUpdaterTest {  
  3.     staticclass A {  
  4.         volatileint intValue = 100;  
  5.     }  
  6.     /** 
  7.      * 可以直接訪問對應的變數,進行修改和處理 
  8.      * 條件:要在可訪問的區域內,如果是private或挎包訪問default型別以及非父親類的protected均無法訪問到 
  9.      * 其次訪問物件不能是static型別的變數(因為在計算屬性的偏移量的時候無法計算),也不能是final型別的變數(因為根本無法修改),必須是普通的成員變數 
  10.      *  
  11.      * 方法(說明上和AtomicInteger幾乎一致,唯一的區別是第一個引數需要傳入物件的引用) 
  12.      * @see AtomicIntegerFieldUpdater#addAndGet(Object, int) 
  13.      * @see AtomicIntegerFieldUpdater#compareAndSet(Object, int, int) 
  14.      * @see AtomicIntegerFieldUpdater#decrementAndGet(Object) 
  15.      * @see AtomicIntegerFieldUpdater#incrementAndGet(Object) 
  16.      *  
  17.      * @see AtomicIntegerFieldUpdater#getAndAdd(Object, int) 
  18.      * @see AtomicIntegerFieldUpdater#getAndDecrement(Object) 
  19.      * @see AtomicIntegerFieldUpdater#getAndIncrement(Object) 
  20.      * @see AtomicIntegerFieldUpdater#getAndSet(Object, int) 
  21.      */
  22.     publicfinalstatic AtomicIntegerFieldUpdater <A>ATOMIC_INTEGER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(A.class"intValue");  
  23.     publicstaticvoid main(String []args) {  
  24.         final A a = new A();  
  25.         for(int i = 0 ; i < 100 ; i++) {  
  26.             finalint num = i;  
  27.             new Thread() {  
  28.                 publicvoid run() {  
  29.                     if(ATOMIC_INTEGER_UPDATER.compareAndSet(a, 100120)) {  
  30.                         System.out.println("我是執行緒:" + num + " 我對對應的值做了修改!");  
  31.                     }  
  32.                 }  
  33.             }.start();  
  34.         }  
  35.     }  
  36. }  

此時你會發現只有一個執行緒可以對這個資料進行修改,其他的方法如上面描述一樣,實現的功能和AtomicInteger類似。

AtomicLongFieldUpdater其實也是這樣,區別在於它所操作的資料是long型別。

AtomicReferenceFieldUpdater方法較少,主要是compareAndSet以及getAndSet兩個方法的使用,它的定義比數字型別的多一個引數如下:

  1. staticclass A {  
  2.     volatile String stringValue = "abc";  
  3. }  
  4. AtomicReferenceFieldUpdater <A ,String>ATOMIC_REFERENCE_FIELD_UPDATER = AtomicReferenceFieldUpdater.newUpdater(A.class, String.class"stringValue");  


可以看到,這裡傳遞的引數增加了一個屬性的型別,因為引用的是一個物件,物件本身也有一個型別。