Java多線程(四) —— 線程並發庫之Atomic
一、從原子操作開始
從相對簡單的Atomic入手(java.util.concurrent是基於Queue的並發包,而Queue,很多情況下使用到了Atomic操作,因此首先從這裏開始)。
很多情況下我們只是需要一個簡單的、高效的、線程安全的遞增遞減方案。註意,這裏有三個條件:
- 簡單,意味著程序員盡可能少的操作底層或者實現起來要比較容易;
- 高效意味著耗用資源要少,程序處理速度要快;
- 線程安全也非常重要,這個在多線程下能保證數據的正確性。
這三個條件看起來比較簡單,但是實現起來卻難以令人滿意。
通常情況下,在Java裏面,++i或者--i不是線程安全的,這裏面有三個獨立的操作:或者變量當前值,為該值+1/-1,然後寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作時“原子性”的。
一切從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差不多,這裏就不介紹了。在下一篇中就介紹下數組、字段等其他方面的原子操作。
二、數組、引用的原子操作
在這一部分開始討論數組原子操作和一些其他的原子操作。
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問題”上很有用。
原子操作的使用大概就是這麽多,大體來說還算是比較清晰的,在下一個章節中,將對象原子操作進行總結,重點介紹下原子操作的原理和設計思想。
三、指令重排序與happens-before法則
在這個小結裏面重點討論原子操作的原理和設計思想。
由於在下一個章節中會談到鎖機制,因此此小節中會適當引入鎖的概念。
在Java Concurrency in Practice中是這樣定義線程安全的:
當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替運行,並且不需要額外的同步及在調用方代碼不必做其他的協調,這個類的行為仍然是正確的,那麽這個類就是線程安全的。
顯然只有資源競爭時才會導致線程不安全,因此無狀態對象永遠是線程安全的。
原子操作的描述是: 多個線程執行一個操作時,其中任何一個線程要麽完全執行完此操作,要麽沒有執行此操作的任何步驟,那麽這個操作就是原子的。
? (求解釋對不對)原子操作不一定是線程安全的:因為每個線程操作的都是自己線程的工作線程的變量副本。
枯燥的定義介紹完了,下面說更枯燥的理論知識。
指令重排序
Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麽指令的執行順序就可能與代碼的順序不一致。這個過程通過叫做指令的重排序。指令重排序存在的意義在於:JVM能夠根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的重新排序機器指令,使機器指令更符合CPU的執行特點,最大限度的發揮機器的性能。
程序執行最簡單的模型是按照指令出現的順序執行,這樣就與執行指令的CPU無關,最大限度的保證了指令的可移植性。這個模型的專業術語叫做順序化一致性模型。但是現代計算機體系和處理器架構都不保證這一點(因為人為的指定並不能總是保證符合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)訪問變量不需要加鎖
四、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就很有用了。這允許一對變化的元素進行原子操作。
Java多線程(四) —— 線程並發庫之Atomic