Java多執行緒(二)之Atomic:原子變數與原子類
一、何謂Atomic?
Atomic一詞跟原子有點關係,後者曾被人認為是最小物質的單位。計算機中的Atomic是指不能分割成若干部分的意思。如果一段程式碼被認為是Atomic,則表示這段程式碼在執行過程中,是不能被中斷的。通常來說,原子指令由硬體提供,供軟體來實現原子方法(某個執行緒進入該方法後,就不會被中斷,直到其執行完成)
在x86 平臺上,CPU提供了在指令執行期間對匯流排加鎖的手段。CPU晶片上有一條引線#HLOCK pin,如果組合語言的程式中在一條指令前面加上字首"LOCK",經過彙編以後的機器程式碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把匯流排鎖住,這樣同一總線上別的CPU就暫時不能通過匯流排訪問記憶體了,保證了這條指令在多處理器環境中的原子性。
二、java.util.concurrent中的原子變數
無論是直接的還是間接的,幾乎 java.util.concurrent
包中的所有類都使用原子變數,而不使用同步。類似 ConcurrentLinkedQueue
的類也使用原子變數直接實現無等待演算法,而類似 ConcurrentHashMap
的類使用 ReentrantLock
在需要時進行鎖定。然後, ReentrantLock
使用原子變數來維護等待鎖定的執行緒佇列。
如果沒有 JDK 5.0 中的 JVM 改進,將無法構造這些類,這些改進暴露了(向類庫,而不是使用者類)介面來訪問硬體級的同步原語。然後,java.util.concurrent 中的原子變數類和其他類向用戶類公開這些功能
java.util.concurrent.atomic的原子類
這個包裡面提供了一組原子類。其基本的特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性,即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,不會阻塞執行緒(或者說只是在硬體級別上阻塞了)。其中的類可以分成4組
- AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
-
AtomicIntegerArray,AtomicLongArray
- AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
- AtomicMarkableReference,AtomicStampedReference,AtomicReferenceArray
其中AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference是類似的。
首先AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference內部api是類似的:舉個AtomicReference的例子
使用AtomicReference建立執行緒安全的堆疊
Java程式碼- public class LinkedStack<T> {
- private AtomicReference<Node<T>> stacks = new AtomicReference<Node<T>>();
- public T push(T e) {
- Node<T> oldNode, newNode;
- while (true) { //這裡的處理非常的特別,也是必須如此的。
- oldNode = stacks.get();
- newNode = new Node<T>(e, oldNode);
- if (stacks.compareAndSet(oldNode, newNode)) {
- return e;
- }
- }
- }
- public T pop() {
- Node<T> oldNode, newNode;
- while (true) {
- oldNode = stacks.get();
- newNode = oldNode.next;
- if (stacks.compareAndSet(oldNode, newNode)) {
- return oldNode.object;
- }
- }
- }
- private static final class Node<T> {
- private T object;
- private Node<T> next;
- private Node(T object, Node<T> next) {
- this.object = object;
- this.next = next;
- }
- }
- }
然後關注欄位的原子更新。
AtomicIntegerFieldUpdater<T>/AtomicLongFieldUpdater<T>/AtomicReferenceFieldUpdater<T,V>是基於反射的原子更新欄位的值。
相應的API也是非常簡單的,但是也是有一些約束的。
(1)欄位必須是volatile型別的!volatile到底是個什麼東西。請檢視 http://blog.csdn.net/a511596982/article/details/8201744
(2)欄位的描述型別(修飾符public/protected/default/private)是與呼叫者與操作物件欄位的關係一致。也就是說呼叫者能夠直接操作物件欄位,那麼就可以反射進行原子操作。但是對於父類的欄位,子類是不能直接操作的,儘管子類可以訪問父類的欄位。
(3)只能是例項變數,不能是類變數,也就是說不能加static關鍵字。
(4)只能是可修改變數,不能使final變數,因為final的語義就是不可修改。實際上final的語義和volatile是有衝突的,這兩個關鍵字不能同時存在。
(5)對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long型別的欄位,不能修改其包裝型別(Integer/Long)。如果要修改包裝型別就需要使用AtomicReferenceFieldUpdater。
在下面的例子中描述了操作的方法。
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問題”上很有用。
三、Atomic類的作用
- 使得讓對單一資料的操作,實現了原子化
-
使用Atomic類構建複雜的,無需阻塞的程式碼
- 訪問對2個或2個以上的atomic變數(或者對單個atomic變數進行2次或2次以上的操作)通常認為是需要同步的,以達到讓這些操作能被作為一個原子單元。
-
無鎖定且無等待演算法
基於 CAS (compare and swap)的併發演算法稱為 無鎖定演算法,因為執行緒不必再等待鎖定(有時稱為互斥或關鍵部分,這取決於執行緒平臺的術語)。無論 CAS 操作成功還是失敗,在任何一種情況中,它都在可預知的時間內完成。如果 CAS 失敗,呼叫者可以重試 CAS 操作或採取其他適合的操作。
如果每個執行緒在其他執行緒任意延遲(或甚至失敗)時都將持續進行操作,就可以說該演算法是 無等待的。與此形成對比的是, 無鎖定演算法要求僅 某個執行緒總是執行操作。(無等待的另一種定義是保證每個執行緒在其有限的步驟中正確計算自己的操作,而不管其他執行緒的操作、計時、交叉或速度。這一限制可以是系統中執行緒數的函式;例如,如果有 10 個執行緒,每個執行緒都執行一次
CasCounter.increment()
操作,最壞的情況下,每個執行緒將必須重試最多九次,才能完成增加。)再過去的 15 年裡,人們已經對無等待且無鎖定演算法(也稱為 無阻塞演算法)進行了大量研究,許多人通用資料結構已經發現了無阻塞演算法。無阻塞演算法被廣泛用於作業系統和 JVM 級別,進行諸如執行緒和程序排程等任務。雖然它們的實現比較複雜,但相對於基於鎖定的備選演算法,它們有許多優點:可以避免優先順序倒置和死鎖等危險,競爭比較便宜,協調發生在更細的粒度級別,允許更高程度的並行機制等等。
常見的:
非阻塞的計數器Counter
非阻塞堆疊ConcurrentStack
非阻塞的連結串列ConcurrentLinkedQueue
ABA問題:
因為在更改 V 之前,CAS 主要詢問“V 的值是否仍為 A”,所以在第一次讀取 V 以及對 V 執行 CAS 操作之前,如果將值從 A 改為 B,然後再改回 A,會使基於 CAS 的演算法混亂。在這種情況下,CAS 操作會成功,但是在一些情況下,結果可能不是您所預期的。這類問題稱為 ABA 問題,通常通過將標記或版本編號與要進行 CAS 操作的每個值相關聯,並原子地更新值和標記,來處理這類問題。AtomicStampedReference
類支援這種方法。