深刻淺出 Java Concurrency (一) ----原子操做

 

 

 

part1 從AtomicInteger開始

 

從相對簡單的Atomic入手(java.util.concurrent是基於Queue的併發包,而Queue,不少狀況下使用到了Atomic操做,所以首先從這裏開始)。不少狀況下咱們只是須要一個簡單的、高效的、線程安全的遞增遞減方案。注意,這裏有三個條件:簡單,意味着程序員儘量少的操做底層或者實現起來要比較容易;高效意味着耗用資源要少,程序處理速度要快;線程安全也很是重要,這個在多線程下能保證數據的正確性。這三個條件看起來比較簡單,可是實現起來卻難以使人滿意。java

一般狀況下,在Java裏面,++i或者--i不是線程安全的,這裏面有三個獨立的操做:或者變量當前值,爲該值+1/-1,而後寫回新的值。在沒有額外資源能夠利用的狀況下,只能使用加鎖才能保證讀-改-寫這三個操做時「原子性」的。程序員

Doug Lea在未將backport-util-concurrent 合併到JSR 166 裏面來以前,是採用純Java實現的,因而不可避免的採用了synchronized關鍵字。算法

public final synchronized void set(int newValue);數組

public final synchronized int getAndSet(int newValue);緩存

public final synchronized int incrementAndGet();安全

同時在變量上使用了volatile (後面會具體來說volatile究竟是個什麼東東)來保證get()的時候不用加鎖。儘管synchronized的代價仍是很高的,可是在沒有JNI的手段下純Java語言仍是不能實現此操做的。數據結構

JSR 166提上日程後,backport-util-concurrent就合併到JDK 5.0裏面了,在這裏面重復使用了現代CPU的特性來下降鎖的消耗。後本章的最後小結中會談到這些原理和特性。在此以前先看看API的使用。多線程

一切從java.util.concurrent.atomic.AtomicInteger開始。架構

int addAndGet(int delta)
          以原子方式將給定值與當前值相加。 實際上就是等於線程安全版本的i =i+delta操做。

boolean compareAndSet(int expect, int update)
          若是當前值 == 預期值,則以原子方式將該值設置爲給定的更新值。 若是成功就返回true,不然返回false,而且不修改原值。

int decrementAndGet()
          以原子方式將當前值減 1。 至關於線程安全版本的--i操做。

int get()
          獲取當前值。

int getAndAdd(int delta)
          以原子方式將給定值與當前值相加。 至關於線程安全版本的t=i;i+=delta;return t;操做。

int getAndDecrement()
          以原子方式將當前值減 1。 至關於線程安全版本的i--操做。

int getAndIncrement()
          以原子方式將當前值加 1。 至關於線程安全版本的i++操做。

int getAndSet(int newValue)
          以原子方式設置爲給定值,並返回舊值。 至關於線程安全版本的t=i;i=newValue;return t;操做。

int incrementAndGet()
          以原子方式將當前值加 1。 至關於線程安全版本的++i操做。 

void lazySet(int newValue)
          最後設置爲給定值。 延時設置變量值,這個等價於set()方法,可是因爲字段是volatile類型的,所以次字段的修改會比普通字段(非volatile字段)有稍微的性能延時(儘管能夠忽略),因此若是不是想當即讀取設置的新值,容許在「後臺」修改值,那麼此方法就頗有用。若是仍是難以理解,這裏就相似於啓動一個後臺線程如執行修改新值的任務,原線程就不等待修改結果當即返回(這種解釋實際上是不正確的,可是能夠這麼理解)。

void set(int newValue)
          設置爲給定值。 直接修改原始值,也就是i=newValue操做。

boolean weakCompareAndSet(int expect, int update)
          若是當前值 == 預期值,則以原子方式將該設置爲給定的更新值。JSR規範中說:以原子方式讀取和有條件地寫入變量但   建立任何 happen-before 排序,所以不提供與除  weakCompareAndSet 目標外任何變量之前或後續讀取或寫入操做有關的任何保證。大意就是說調用weakCompareAndSet時並不能保證不存在happen-before的發生(也就是可能存在指令重排序致使此操做失敗)。可是從Java源碼來看,其實此方法並無實現JSR規範的要求,最後效果和compareAndSet是等效的,都調用了unsafe.compareAndSwapInt()完成操做。


下面的代碼是一個測試樣例,爲了省事就寫在一個方法裏面來了。
 package  xylz.study.concurrency.atomic;
 
 import  java.util.concurrent.atomic.AtomicInteger;
 
 import  org.junit.Test;
 
 import   static  org.junit.Assert. * ;
 
 public   class  AtomicIntegerTest   {
 
     @Test
      public   void  testAll()  throws  InterruptedException {
          final  AtomicInteger value  =   new  AtomicInteger( 10 );
         assertEquals(value.compareAndSet( 1 ,  2 ),  false );
         assertEquals(value.get(),  10 );
         assertTrue(value.compareAndSet( 10 ,  3 ));
         assertEquals(value.get(),  3 );
         value.set( 0 );
          //
         assertEquals(value.incrementAndGet(),  1 );
         assertEquals(value.getAndAdd( 2 ), 1 );
         assertEquals(value.getAndSet( 5 ), 3 );
         assertEquals(value.get(), 5 );
          //
          final   int  threadSize  =   10 ;
         Thread[] ts  =   new  Thread[threadSize];
          for  ( int  i  =   0 ; i  <  threadSize; i ++ )  {
             ts[i]  =   new  Thread()  {
                  public   void  run()  {
                     value.incrementAndGet();
                 } 
             } ;
         } 
          //
          for (Thread t:ts)  {
             t.start();
         } 
          for (Thread t:ts)  {
             t.join();
         } 
          //
         assertEquals(value.get(),  5 + threadSize);
     } 
 
 } 

 


 因爲這裏例子比較簡單,這裏就不作過多介紹了。
AtomicInteger和AtomicLong、AtomicBoolean、AtomicReference差很少,這裏就不介紹了。在下一篇中就介紹下數組、字段等其餘方面的原子操做。
 參考資料:
(1)http://stackoverflow.com/questions/2443239/java-atomicinteger-what-are-the-differences-between-compareandset-and-weakcompar 
(2)http://stackoverflow.com/questions/1468007/atomicinteger-lazyset-and-set

 

part 2 數組、引用的原子操做

 

 

在這一部分開始討論數組原子操做和一些其餘的原子操做。

AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray 的API相似,選擇有表明性的AtomicIntegerArray來描述這些問題。

int get(int i)

獲取位置  i   的當前值。很顯然,因爲這個是數組操做,就有索引越界的問題(IndexOutOfBoundsException異常)。

 

對於下面的API起始和AtomicInteger是相似的,這種經過方法、參數的名稱就可以獲得函數意義的寫法是很是值得稱讚的。在《重構:改善既有代碼的設計》 和《代碼整潔之道》 中都很是推崇這種作法。


void set(int i, int newValue) 
void lazySet(int i, int newValue)
int getAndSet(int i, int newValue)
boolean compareAndSet(int i, int expect, int update)
boolean weakCompareAndSet(int i, int expect, int update)
int getAndIncrement(int i)
int getAndDecrement(int i)
int getAndAdd(int i, int delta)
int incrementAndGet(int i)
int decrementAndGet(int i)
int addAndGet(int i, int delta)

 

總體來講,數組的原子操做在理解上仍是相對比較容易的,這些API就是有多使用才能體會到它們的好處,而不只僅是停留在理論階段。

如今關注字段的原子更新。

AtomicIntegerFieldUpdater<T>/AtomicLongFieldUpdater<T>/AtomicReferenceFieldUpdater<T,V> 是基於反射的原子更新字段的值。

相應的API也是很是簡單的,可是也是有一些約束的。

(1)字段必須是volatile類型的!在後面的章節中會詳細說明爲何必須是volatile,volatile究竟是個什麼東西。

(2)字段的描述類型(修飾符public/protected/default/private)是與調用者與操做對象字段的關係一致。也就是說調用者可以直接操做對象字段,那麼就能夠反射進行原子操做。可是對於父類的字段,子類是不能直接操做的,儘管子類能夠訪問父類的字段。

(3)只能是實例變量,不能是類變量,也就是說不能加static關鍵字。

(4)只能是可修改變量,不能使final變量,由於final的語義就是不可修改。實際上final的語義和volatile是有衝突的,這兩個關鍵字不能同時存在。

(5)對於AtomicIntegerFieldUpdater 和AtomicLongFieldUpdater 只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。若是要修改包裝類型就須要使用AtomicReferenceFieldUpdater 。

 

在下面的例子中描述了操做的方法。

 

 package   xylz.study.concurrency.atomic;  
 
 import   java.util.concurrent.atomic.AtomicIntegerFieldUpdater;  
 
 public   class   AtomicIntegerFieldUpdaterDemo   {  
 
     class   DemoData {
         public   volatile   int   value1  =   1 ;
         volatile   int   value2  =   2 ;
         protected   volatile   int   value3  =   3 ;
         private   volatile   int   value4  =   4 ;
    } 
     AtomicIntegerFieldUpdater < DemoData >   getUpdater(String fieldName)  {
          return   AtomicIntegerFieldUpdater.newUpdater(DemoData. class , fieldName);
     } 
      void   doit()  {
         DemoData data  =   new   DemoData();
         System.out.println( " 1 ==>  " + getUpdater( " value1 " ).getAndSet(data,  10 ));
         System.out.println( " 3 ==>  " + getUpdater( " value2 " ).incrementAndGet(data));
         System.out.println( " 2 ==>  " + getUpdater( " value3 " ).decrementAndGet(data));
         System.out.println( " true ==>  " + getUpdater( " value4 " ).compareAndSet(data,  4 ,  5 ));
     } 
      public   static   void   main(String[] args)  {
         AtomicIntegerFieldUpdaterDemo demo  =   new   AtomicIntegerFieldUpdaterDemo();
         demo.doit();
     } 
 }  
 


在上面的例子中DemoData的字段value3/value4對於AtomicIntegerFieldUpdaterDemo類是不可見的,所以經過反射是不能直接修改其值的。

 

AtomicMarkableReference 類描述的一個<Object,Boolean>的對,能夠原子的修改Object或者Boolean的值,這種數據結構在一些緩存或者狀態描述中比較有用。這種結構在單個或者同時修改Object/Boolean的時候可以有效的提升吞吐量。

 

AtomicStampedReference 類維護帶有整數「標誌」的對象引用,能夠用原子方式對其進行更新。對比AtomicMarkableReference 類的<Object,Boolean>,AtomicStampedReference 維護的是一種相似<Object,int>的數據結構,其實就是對對象(引用)的一個併發計數。可是與AtomicInteger 不一樣的是,此數據結構能夠攜帶一個對象引用(Object),而且可以對此對象和計數同時進行原子操做。

在後面的章節中會提到「ABA問題」,而AtomicMarkableReference/ AtomicStampedReference 在解決「ABA問題」上頗有用

part3 指令重排序與happens-before法則

 

在這個小結裏面重點討論原子操做的原理和設計思想。

因爲在下一個章節中會談到鎖機制,所以此小節中會適當引入鎖的概念。

Java Concurrency in Practice 中是這樣定義線程安全的:

當多個線程訪問一個類時,若是不用考慮這些線程在運行時 環境下的調度和交替運行,而且不須要額外的同步及在調用方代碼沒必要作其餘的協調 ,這個類的行爲仍然是正確的 ,那麼這個類就是線程安全的。

顯然只有資源競爭時纔會致使線程不安全,所以無狀態對象永遠是線程安全的 

原子操做的描述是: 多個線程執行一個操做時,其中任何一個線程要麼徹底執行完此操做,要麼沒有執行此操做的任何步驟 ,那麼這個操做就是原子的。

枯燥的定義介紹完了,下面說更枯燥的理論知識。

指令重排序

Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。這個過程經過叫作指令的重排序。指令重排序存在的意義在於:JVM可以根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的從新排序機器指令,使機器指令更符合CPU的執行特色,最大限度的發揮機器的性能。

程序執行最簡單的模型是按照指令出現的順序執行,這樣就與執行指令的CPU無關,最大限度的保證了指令的可移植性。這個模型的專業術語叫作順序化一致性模型。可是現代計算機體系和處理器架構都不保證這一點(由於人爲的指定並不能老是保證符合CPU處理的特性)。

咱們來看最經典的一個案例。

 package   xylz.study.concurrency.atomic;  
 
 public   class   ReorderingDemo   {  
 
      static   int   x  =   0 , y  =   0 , a  =   0 , b  =   0 ;  
 
      public   static   void   main(String[] args)  throws   Exception  {  
 
          for   ( int   i  =   0 ; i  <   100 ; i ++ )  {
             x = y = a = b = 0 ;
             Thread one  =   new   Thread()  {
                  public   void   run()  {
                     a  =   1 ;
                     x  =   b;
                 } 
             } ;
             Thread two  =   new   Thread()  {
                  public   void   run()  {
                     b  =   1 ;
                     y  =   a;
                 } 
             } ;
             one.start();
             two.start();
             one.join();
             two.join();
             System.out.println(x  +   "   "   +   y);
         } 
     }  
 
 } 
 

在這個例子中one/two兩個線程修改區x,y,a,b四個變量,在執行100次的狀況下,可能獲得(0 1)或者(1 0)或者(1 1)。事實上按照JVM的規範以及CPU的特性有極可能獲得(0 0)。固然上面的代碼你們不必定能獲得(0 0),由於run()裏面的操做過於簡單,可能比啓動一個線程花費的時間還少,所以上面的例子難以出現(0,0)。可是在現代CPU和JVM上確實是存在的。因爲run()裏面的動做對於結果是無關的,所以裏面的指令可能發生指令重排序,即便是按照程序的順序執行,數據變化刷新到主存也是須要時間的。假定是按照a=1;x=b;b=1;y=a;執行的,x=0是比較正常的,雖然a=1在y=a以前執行的,可是因爲線程one執行a=1完成後尚未來得及將數據1寫回主存(這時候數據是在線程one的堆棧裏面的),線程two從主存中拿到的數據a可能仍然是0(顯然是一個過時數據,可是是有可能的),這樣就發生了數據錯誤。

在兩個線程交替執行的狀況下數據的結果就不肯定了,在機器壓力大,多核CPU併發執行的狀況下,數據的結果就更加不肯定了。

Happens-before法則

Java存儲模型有一個happens-before原則,就是若是動做B要看到動做A的執行結果(不管A/B是否在同一個線程裏面執行),那麼A/B就須要知足happens-before關係。

在介紹happens-before法則以前介紹一個概念:JMM動做(Java Memeory Model Action),Java存儲模型動做。一個動做(Action)包括:變量的讀寫、監視器加鎖和釋放鎖、線程的start()和join()。後面還會提到鎖的的。

happens-before完整規則:

(1)同一個線程中的每一個Action都happens-before於出如今其後的任何一個Action。

(2)對一個監視器的解鎖happens-before於每個後續對同一個監視器的加鎖。

(3)對volatile字段的寫入操做happens-before於每個後續的同一個字段的讀操做。

(4)Thread.start()的調用會happens-before於啓動線程裏面的動做。

(5)Thread中的全部動做都happens-before於其餘線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。

(6)一個線程A調用另外一個另外一個線程B的interrupt()都happens-before於線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。

(7)一個對象構造函數的結束happens-before與該對象的finalizer的開始

(8)若是A動做happens-before於B動做,而B動做happens-before與C動做,那麼A動做happens-before於C動做。

volatile語義

到目前爲止,咱們屢次提到volatile,可是卻仍然沒有理解volatile的語義。

volatile至關於synchronized的弱實現,也就是說volatile實現了相似synchronized的語義,卻又沒有鎖機制。它確保對volatile字段的更新以可預見的方式告知其餘的線程。

volatile包含如下語義:

(1)Java 存儲模型不會對valatile指令的操做進行重排序:這個保證對volatile變量的操做時按照指令的出現順序執行的。

(2)volatile變量不會被緩存在寄存器中(只有擁有線程可見)或者其餘對CPU不可見的地方,每次老是從主存中讀取volatile變量的結果。也就是說對於volatile變量的修改,其它線程老是可見的,而且不是使用本身線程棧內部的變量。也就是在happens-before法則中,對一個valatile變量的寫操做後,其後的任何讀操做理解可見此寫操做的結果。

儘管volatile變量的特性不錯,可是volatile並不能保證線程安全的,也就是說volatile字段的操做不是原子性的,volatile變量只能保證可見性(一個線程修改後其它線程可以理解看到此變化後的結果),要想保證原子性,目前爲止只能加鎖!

volatile一般在下面的場景:

 

 volatile   boolean   done  =   false ;
 
 …
 
      while (  !   done )  {
         dosomething();
     }

 

應用volatile變量的三個原則:

(1)寫入變量不依賴此變量的值,或者只有一個線程修改此變量

(2)變量的狀態不須要與其它變量共同參與不變約束

(3)訪問變量不須要加鎖

 

這一節理論知識比較多,可是這是很面不少章節的基礎,在後面的章節中會屢次提到這些特性。

本小節中仍是沒有談到原子操做的原理和思想,在下一節中將根據上面的一些知識來介紹原子操做。

 

參考資料:

(1)Java Concurrency in Practice

(2)正確使用 Volatile 變量

 

 

part 4 CAS操做

 

在JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這會致使有鎖(後面的章節還會談到鎖)。

鎖機制存在如下問題:

(1)在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。

(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起。

(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。

volatile是不錯的機制,可是volatile不能保證原子性。所以對於同步最終仍是要回到鎖機制上來。

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。

CAS 操做

上面的樂觀鎖用到的機制就是CAS,Compare and Swap。

CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

非阻塞算法 (nonblocking algorithms)

一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。

現代的CPU提供了特殊的指令,能夠自動更新共享數據,並且可以檢測到其餘線程的干擾,而 compareAndSet() 就用這些代替了鎖定。

拿出AtomicInteger來研究在沒有鎖的狀況下是如何作到數據正確性的。

private volatile int value;

首先毫無覺得,在沒有鎖的機制下可能須要藉助volatile原語,保證線程間的數據是可見的(共享的)。

這樣才獲取變量的值的時候才能直接讀取。

public final int get() {
        return value;
    }

而後來看看++i是怎麼作到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。

而compareAndSet利用JNI來完成CPU指令的操做。

public final boolean compareAndSet(int expect, int update) {    
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

總體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

而整個J.U.C都是創建在CAS之上的,所以對於synchronized阻塞算法,J.U.C在性能上有了很大的提高。參考資料的文章中介紹了若是利用CAS構建非阻塞計數器、隊列等數據結構。

CAS看起來很爽,可是會致使「ABA問題」。

CAS算法實現一個重要前提須要取出內存中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會致使數據的變化。

好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。若是鏈表的頭在變化了兩次後恢復了原值,可是不表明鏈表就沒有變化。所以前面提到的原子操做AtomicStampedReference/AtomicMarkableReference就頗有用了。這容許一對變化的元素進行原子操做。

 

 

參考資料:

(1)非阻塞算法簡介

(2)流行的原子

相關文章
相關標籤/搜索