JUC併發工具九-Atomic類

目錄

 

1 Atomic類作用

2 AtomicInteger使用

2.1 AtomicInteger用法

2.2 AtomicInteger原理

2.2.1 重要成員變量

2.2.2 addAndGet方法

2.3 AtomicInteger優缺點

3 AtomicReference使用

3.1 AtomicReference用法

3.2 AtomicReference原理

3.2.1 重要成員變量

3.2.3 updateAndGet方法

3.3.3 AtomicReference使用誤區

4 ABA問題以及AtomicStampedReference的使用

4.1 ABA問題

4.2 AtomicStampedReference使用

4.3 AtomicStampedReference原理

4.3.1 重要成員變量

4.3.2 compareAndSet方法


1 Atomic類作用

前面文章我們知道volatile只能保證可見性,不能保證原子性,下面我們通過代碼證明一下

// 定義一個volatile變量
private volatile int value = 0;

@Test
public void volatileTest() throws InterruptedException {
    // 循環10000次,每次自增1
    for (int i = 0; i < 10000; i ++) {
        new Thread(() -> value ++).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(value);
}

通過測試我們可以看到程序輸出結果不確定的,大都達不到10000,可以說明transient不能保證原子性。
爲了滿足jvm內存模型原子性特性,juc併發包提供了Atomic類來保證原子性。

2 AtomicInteger使用

2.1 AtomicInteger用法

同樣的功能我們用AtomicInteger來測試

private AtomicInteger value = new AtomicInteger();

@Test
public void atomicIntegerTest() throws InterruptedException {
    for (int i = 0; i < 10000; i ++) {
        new Thread(() -> value.addAndGet(1)).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(value);
}

每次輸出的結果都是10000,可以說明AtomicInteger保證原子性

2.2 AtomicInteger原理

AtomicInteger原理也比較簡單,就是前面文章講過的CAS自旋volatile變量。

2.2.1 重要成員變量

// 定義一個volatile變量
private volatile int value;

// value屬性的內存偏移量,在static塊中初始化
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

2.2.2 addAndGet方法

public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

/**
* var1:當前對象
* var2:某個屬性的內存地址偏移量
* var4: 增量
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 通過當前對象和和某個屬性內存偏移量讀取屬性值
        var5 = this.getIntVolatile(var1, var2);
        // 循環給對象賦值,直到成功
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

2.3 AtomicInteger優缺點

優點:無鎖,效率高
缺點:cpu佔用較高,只能保證一個共享變量原子操作,ABA問題
解決方案
    1 無解,高性能是建立在高CPU佔用的基礎上實現的
    2 AtomicReference
    3 AtomicStampedReference (多了一個stamp變量,可以理解爲版本號)

3 AtomicReference使用

3.1 AtomicReference用法

創建一個員工類,有一個id和age屬性,多線程對這個員工的id和age同時+1,循環10000次,觀察是否會有併發問題

@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 7773021982257206440L;

    private int id;
    
    private int age;

測試代碼

@Test
public void atomicReferenceTest() throws InterruptedException {
    for (int i = 0; i < 10000; i ++) {
        new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
            Employee newEmp = new Employee();
            newEmp.setId(employee.getId() + 1);
            newEmp.setAge(employee.getAge() + 1);
            return newEmp;
        })).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(employeeAtomicReference.get());
}

輸出Employee{id=10000, age=10000}

3.2 AtomicReference原理

3.2.1 重要成員變量

// 定義一個泛型volatile變量
private volatile V value;

// value屬性的內存偏移量,在static塊中初始化
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

3.2.3 updateAndGet方法

public final V updateAndGet(UnaryOperator<V> updateFunction) {
    V prev, next;
    do {
        // 獲取當前對象
        prev = get();
        // 獲取執行後的對象
        next = updateFunction.apply(prev);
        // CAS更新對象爲next
    } while (!compareAndSet(prev, next));
    return next;
}

/**
* expect 希望的當前值
* update 要修改的值
*/
public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

3.3.3 AtomicReference使用誤區

在使用AtomicReference的時候做對象操作的時候一定要用一個新建的對象,否則不能保證原子性
錯誤用例:

@Test
public void atomicReferenceTest() throws InterruptedException {
    for (int i = 0; i < 10000; i ++) {
        new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
            // 這裏直接用原來的對象進行修改
            employee.setId(employee.getId() + 1);
            employee.setAge(employee.getAge() + 1);
            return employee;
        })).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println(employeeAtomicReference.get());
}

輸出:Employee{id=9995, age=9996}
問題原因:
看CAS參數compareAndSet(prev, next)方法,prev參數和next參數實際上都是AtomicReference內部引用的employee對象
所以該方法始終返回true,達不到併發控制效果

4 ABA問題以及AtomicStampedReference的使用

4.1 ABA問題

整個CAS過程分爲3步,查詢當前值,比較希望的值(第一步取到的當前值)和當前值,修改。這裏假設有3個線程操作數據

上個流程圖可知線程一先獲取數據,然後掛起。
線程二和線程三修改數據,但是值沒有變。
這個時候線程一不能感知數據已經被修改過,進行CAS修改成功。這就是ABA問題

4.2 AtomicStampedReference使用

AtomicStampedReference解決ABA問題的方法也很簡單,在原有的CAS方法在expectValue的基礎上多了一個expectStamp條件的判斷,
不僅expectedValue要和當前值相等,expectedStamp也要和當前值相等。
測試代碼

private AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);

@Test
public void atomicReferenceTest() throws InterruptedException {
    System.out.println(value.compareAndSet(0, 1, 1, 1));
    System.out.println(value.getReference() + " " + value.getStamp());
    System.out.println(value.compareAndSet(0, 1, 0, 1));
    System.out.println(value.getReference() + " " + value.getStamp());
}

輸出
false
0 0
true
1 1

4.3 AtomicStampedReference原理

4.3.1 重要成員變量

// 定義一個靜態內部類,維護reference和stamp兩個屬性
private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

// 定義一個volatile的Pair對象
private volatile Pair<V> pair;

private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

4.3.2 compareAndSet方法

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    // CAS的時候要保證expectedReference和當前reference相等,expectedStamp和當前stamp相等,才能進行CAS處理
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

// CAS修改成員變量pair
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

 

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