Java併發--原子變量類的使用

:本篇博客主要內容來源於網絡,侵刪~

引言

我們假設你已經熟練掌握了CAS,原子變量類等的相關概念。這篇博客中,我們主要討論原子變量類的使用。


原子變量類

原子變量類共12個,分4組:

  1. 計數器:AtomicIntegerAtomicLongAtomicBooleanAtomicReference
  2. 域更新器:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
  3. 數組:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
  4. 複合變量:AtomicMarkableReferenceAtomicStampedReference

在每組中我會選擇其中一個較有代表性的進行分析與舉例。

AtomicReference

使用說明

AtomicReference的作用是對"對象"進行原子操作。

源碼分析
public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;
 
    // 獲取Unsafe對象,Unsafe的作用是提供CAS操作
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
 
    static {
        try {
            // 獲取相應字段相對Java對象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
 
    // volatile類型
    private volatile V value;
 
    public AtomicReference(V initialValue) {
        value = initialValue;
    }
 
    public AtomicReference() {
    }
 
    public final V get() {
        return value;
    }
 
    public final void set(V newValue) {
        value = newValue;
    }
 
    public final void lazySet(V newValue) {
        unsafe.putOrderedObject(this, valueOffset, newValue);
    }
 
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
 
    public final boolean weakCompareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
 
    public final V getAndSet(V newValue) {
        while (true) {
            V x = get();
            if (compareAndSet(x, newValue))
                return x;
        }
    }
 
    public String toString() {
        return String.valueOf(get());
    }
}

關於上述代碼只有兩點需要強調:

  1. valueOffset = unsafe.objectFieldOffset(AtomicReference.class.getDeclaredField("value")) 通過相關字段的偏移量獲取值比直接使用反射獲取相應字段的值性能要好許多;
  2. 關於lazySet,推薦閱讀這一篇博客:JUC中Atomic Class之lazySet的一點疑惑
使用舉例
class Person {
    volatile long id;
    
    public Person(long id) {
        this.id = id;
    }
    
    public String toString() {
        return "id:" + id;
    }
}

public class AtomicReferenceTest {
    public static void main(String[] args) {
        // 創建兩個Person對象,它們的id分別是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference對象,初始化它的值爲p1對象
        AtomicReference ar = new AtomicReference(p1);
        
        // 通過CAS設置ar。如果ar的值爲p1的話,則將其設置爲p2。
        ar.compareAndSet(p1, p2);
 
        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
    }
}

AtomicReferenceFieldUpdater

接下來所有的原子變量類不再進行源碼分析。事實上所有原子變量類的實現都大同小異,感興趣的同學可以閱讀源碼。

使用說明

一個基於反射的工具類,它能對指定類的指定的volatile引用字段進行原子更新。(注意這個字段不能是private的)

通過調用AtomicReferenceFieldUpdater的靜態方法newUpdater就能創建它的實例,該方法要接收三個參數:

  1. 包含該字段的對象的類;
  2. 將被更新的對象的類;
  3. 將被更新的字段的名稱。
使用舉例
class Dog {  
    volatile String name = "dog1";
}  

public class App {
    public static void main(String[] args) throws Exception {
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
        Dog dog1 = new Dog();
        updater.compareAndSet(dog1, dog1.name, "test");
        
        System.out.println(dog1.name);
    }
}

AtomicReferenceArray

使用說明

可以用原子方式更新其元素的對象引用數組。

以下是AtomicReferenceArray類中可用的重要方法的列表:

序列 方法 描述
1 public AtomicReferenceArray(int length) 構造函數,創建給定長度的新 AtomicReferenceArray。
2 public AtomicReferenceArray(E[] array) 構造函數,創建與給定數組具有相同長度的新 AtomicReferenceArray,並從給定數組複製其所有元素。
3 public boolean compareAndSet(int i, E expect, E update) 如果當前值==期望值,則將位置i處的元素原子設置爲給定的更新值。
4 public E get(int i) 獲取位置i的當前值。
5 public E getAndSet(int i, E newValue) 將位置i處的元素原子設置爲給定值,並返回舊值。
6 public void set(int i, E newValue) 將位置i處的元素設置爲給定值。
使用舉例
public class TestThread {
    // 創建原子引用數組
    private static String[] source = new String[10];
    private static AtomicReferenceArray<String> atomicReferenceArray = new AtomicReferenceArray<String>(source);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < atomicReferenceArray.length(); i++) {
            atomicReferenceArray.set(i, "item-2");
        }

        Thread t1 = new Thread(new Increment());
        Thread t2 = new Thread(new Compare());
        t1.start();
        t2.start();

        t1.join();
        t2.join();        
    }
    
    static class Increment implements Runnable {
        public void run() {
            for(int i = 0; i < atomicReferenceArray.length(); i++) {
                String add = atomicReferenceArray.getAndSet(i, "item-" + (i+1));
                System.out.println("Thread " + Thread.currentThread().getId() 
                    + ", index " + i + ", value: " + add);
            }
        }
    }
    
    static class Compare implements Runnable {
        public void run() {
            for(int i = 0; i< atomicReferenceArray.length(); i++) {
                System.out.println("Thread " + Thread.currentThread().getId() 
                    + ", index " + i + ", value: " + atomicReferenceArray.get(i));
                boolean swapped = atomicReferenceArray.compareAndSet(i, "item-2", "updated-item-2");
                System.out.println("Item swapped: " + swapped);
                if(swapped) {
                    System.out.println("Thread " + Thread.currentThread().getId() 
                        + ", index " + i + ", updated-item-2");
                }
            }
        }
    }
}

AtomicStampedReference

使用說明

AtomicStampedReference主要用來解決在使用CAS算法的過程中,可能會產生的ABA問題。一般我們會使用帶有版本戳version的記錄或對象標記來解決ABA問題,AtomicStampedReference實現了這個作用,它通過包裝[E, Integer]的元組來對對象標記版本戳stamp。

以下是AtomicStampedReference類中可用的重要方法的列表:

序列 方法 描述
1 public AtomicStampedReference(V initialRef, int initialStamp) 構造方法,傳入引用和戳。
2 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) 如果當前引用 == 預期值並且當前版本戳 == 預期版本戳,將更新新的引用和新的版本戳到內存。
3 public void set(V newReference, int newStamp) 設置當前引用的新引用和版本戳。
4 public boolean attemptStamp(V expectedReference, int newStamp) 如果當前引用 == 預期引用,將更新新的版本戳到內存。
使用舉例

下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值爲100的原子整型變量進行更新,AtomicInteger會成功執行CAS操作,而加上版本戳的AtomicStampedReference對於ABA問題會執行CAS失敗:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);

    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });

        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);         // true
            }
        });

        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();

        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            }
        });

        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                }
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                System.out.println(c3);         // false
            }
        });

        refT1.start();
        refT2.start();
    }
}

性能比較:鎖與原子變量

事實上,CAS的性能總是優於鎖。我們分兩種情況進行討論。

1. 線程間競爭程度較高

對於鎖來說,激烈的競爭意味着線程頻繁的掛起與恢復,頻繁的上下文切換,這些操作都是非常耗費系統資源的;對於CAS算法來說,激烈的競爭意味着線程將對競爭進行頻繁的處理(重試,回退,放棄等策略)。

即使如此,一般來說,CAS算法的性能依舊優於鎖。

2. 線程間競爭程度較低

較低的競爭程度意味着CAS操作總是能夠成功;對於鎖來說,雖然鎖之間的競爭度也隨之下降,但由於獲取鎖與釋放鎖的操作不但耗費系統資源,並且其中本身就包含着CAS操作,因此在這種情況下,CAS操作的性能依舊優於鎖。


總結

  1. 這篇博客並沒有講述CAS操作以及可能產生的ABA問題,但是我們必須熟悉這兩個知識點;
  2. 這篇博客的主要目的是構建起大家對原子變量類的一個認識,以至於在以後的項目開發中,可以去思考如何使用這些原子變量類;
  3. 對於原子變量與鎖之間的優勢與劣勢,性能間的比較,有一個較爲清晰的認識。

參考閱讀

Java併發編程實戰

Java併發AtomicReferenceArray類

用AtomicStampedReference解決ABA問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章