【Java併發編程】併發操做原子類Atomic以及CAS的ABA問題

本文基於JDK1.8java

Atomic原子類

原子類是具備原子操做特徵的類。c++

原子類存在於java.util.concurrent.atmic包下。編程

根據操做的數據類型,原子類能夠分爲如下幾類。數組

基本類型

  • AtomicInteger:整型原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean:布爾型原子類

AtomicInteger的經常使用方法

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //加上給定的值,並返回以前的值
public final int addAndGet(int delta) //加上給定的值,並返回最終結果
boolean compareAndSet(int expect, int update) //若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

AtomicInteger常見方法的使用

@Test
public void AtomicIntegerT() {

    AtomicInteger c = new AtomicInteger();

    c.set(10);
    System.out.println("初始設置的值 ==>" + c.get());

    int andAdd = c.getAndAdd(10);
    System.out.println("爲原先的值加上10,並返回原先的值,原先的值是 ==> " + andAdd + "加上以後的值是 ==> " + c.get());

    int finalVal = c.addAndGet(5);
    System.out.println("加上5, 以後的值是 ==> " + finalVal);

    int i = c.incrementAndGet();
    System.out.println("++1,以後的值爲 ==> " + i);
    
    int result = c.updateAndGet(e -> e + 3);
    System.out.println("可使用函數式更新 + 3 計算後的結果爲 ==> "+ result);

    int res = c.accumulateAndGet(10, (x, y) -> x + y);
    System.out.println("使用指定函數計算後的結果爲 ==>" + res);
}

初始設置的值 ==>10
爲原先的值加上10,並返回原先的值,原先的值是 ==> 10 
加上以後的值是 ==> 20
加上5, 以後的值是 ==> 25
++1,以後的值爲 ==> 26
可使用函數式更新 + 3 計算後的結果爲 ==> 29
使用指定函數計算後的結果爲 ==>39

AtomicInteger保證原子性

咱們知道,volatile能夠保證可見性和有序性,可是不能保證原子性,所以,如下的代碼在併發環境下的結果會不正確:最終的結果可能會小於10000。併發

public class AtomicTest {

    static CountDownLatch c = new CountDownLatch(10);
    public volatile int inc = 0;

    public static void main(String[] args) throws InterruptedException {

        final AtomicTest test = new AtomicTest();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
                c.countDown();
            }).start();
        }
        c.await();
        System.out.println(test.inc);

    }
    //不是原子操做, 先讀取inc的值, inc + 1, 寫回內存
    public void increase() {
        inc++;
    }
}

想要解決最終結果不是10000的辦法有兩個:ide

  • 使用synchronized關鍵字,修飾increase方法,鎖能夠保證該方法某一時刻只能有一個線程執行,保證了原子性。
public synchronized void increase() {
        inc++;
    }
  • 使用Atomic原子類,好比這裏的AtomicInteger
public class AtomicTest {

    static CountDownLatch c = new CountDownLatch(10);

    // 使用整型原子類 保證原子性
    public AtomicInteger inc = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {

        final AtomicTest test = new AtomicTest();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
                c.countDown();
            }).start();
        }
        c.await();
        System.out.println(test.getCount());
    }

    // 獲取當前的值,並自增
    public void increase() {
        inc.getAndIncrement();
    }

    // 獲取當前的值
    public int getCount() {
        return inc.get();
    }
}

getAndIncrement()方法的實現

getAndIncrement方法是如何確保原子操做的呢?函數

private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            //objectFieldOffset本地方法,用來拿到「原來的值」的內存地址。
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//value在內存中可見,JVM能夠保證任什麼時候刻任何線程總能拿到該變量的最新值
    private volatile int value;   


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

openjdk1.8Unsafe類的源碼:Unsafe.java高併發

/**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object <code>o</code>
     * at the given <code>offset</code>.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

Java的源碼改動是有的,《Java併發編程的藝術》的內容也在此摘錄一下,相對來講更好理解一些:性能

public final int getAddIncrement() {
        for ( ; ; ) {
            //先取得存儲的值
            int current = get();
            //加1操做
            int next = current + 1;
            // CAS保證原子更新操做,若是輸入的數值等於預期值,將值設置爲輸入的值
            if (compareAndSet(current, next)) {
                return current;
            }
        }
    }

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

數組類型

  • AtomicIntegerArray:整型數組原子類
  • AtomicLongArray:長整型數組原子類
  • AtomicReferenceArray :引用類型數組原子類

AtomicIntegerArray的經常使用方法

@Test
public void AtomicIntegerArrayT() {

    int[] nums = {1, 2, 3, 4, 5};
    AtomicIntegerArray c = new AtomicIntegerArray(nums);

    for (int i = 0; i < nums.length; i++) {
        System.out.print(c.get(i) + " ");
    }
    System.out.println();

    int finalVal = c.addAndGet(0, 10);
    System.out.println("索引爲 0 的值 加上 10  ==> " + finalVal);

    int i = c.incrementAndGet(0);
    System.out.println("索引爲 0 的值 ++1,以後的值爲 ==> " + i);

    int result = c.updateAndGet(0, e -> e + 3);
    System.out.println("可使用函數式更新索引爲0 的位置 + 3 計算後的結果爲 ==> " + result);

    int res = c.accumulateAndGet(0, 10, (x, y) -> x * y);
    System.out.println("使用指定函數計算後的結果爲 ==> " + res);
}

引用類型

基本類型原子類只能更新一個變量,若是須要原子更新多個變量,須要使用 引用類型原子類。ui

  • AtomicReference:引用類型原子類
  • AtomicMarkableReference:原子更新帶有標記的引用類型,沒法解決ABA問題,該類的標記更多用於表示引用值是否已邏輯刪除
  • AtomicStampedReference :原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,能夠解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

AtomicReference常見方法的使用

@Test
public void AtomicReferenceT(){

    AtomicReference<Person> ar = new AtomicReference<>();
    Person p = new Person(18,"summer");

    ar.set(p);

    Person pp = new Person(50,"dan");
    ar.compareAndSet(p, pp);// except = p  update = pp

    System.out.println(ar.get().getName());
    System.out.println(ar.get().getAge());

}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{

    int age;
    String name;
}
//dan
//50

對象的屬性修改類型

若是須要原子更新某個類裏的某個字段時,須要用到對象的屬性修改類型原子類。

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新長整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用類型裏的字段

要想原子地更新對象的屬性須要兩步。

  1. 由於對象的屬性修改類型原子類都是抽象類,因此每次使用都必須使用靜態方法 newUpdater()建立一個更新器,而且須要設置想要更新的類和屬性。
  2. 更新的對象屬性必須使用 public volatile 修飾符。

AtomicIntegerFieldUpdater經常使用方法的使用

@Test
public void AtomicIntegerFieldUpdateTest(){
    AtomicIntegerFieldUpdater<Person> a =
        AtomicIntegerFieldUpdater.newUpdater(Person.class,"age");
    Person p = new Person(18,"summer");
    System.out.println(a.getAndIncrement(p)); //18
    System.out.println(a.get(p)); //19
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{

    public volatile int age;
    private String name;
}

Java8新增的原子操做類

  • LongAdder

因爲AtomicLong經過CAS提供非阻塞的原子性操做,性能已經很好,在高併發下大量線程競爭更新同一個原子量,但只有一個線程可以更新成功,這就形成大量的CPU資源浪費。

LongAdder 經過讓多個線程去競爭多個Cell資源,來解決,再很高的併發狀況下,線程操做的是Cell數組,並非base,在cell元素不足時進行2倍擴容,在高併發下性能高於AtomicLong

CAS的ABA問題的產生

假設兩個線程訪問同一變量x。

  1. 第一個線程獲取到了變量x的值A,而後執行本身的邏輯。
  2. 這段時間內,第二個線程也取到了變量x的值A,而後將變量x的值改成B,而後執行本身的邏輯,最後又把變量x的值變爲A【還原】。
  3. 在這以後,第一個線程終於進行了變量x的操做,但此時變量x的值仍是A,覺得x的值沒有變化,因此compareAndSet仍是會成功執行。

先來看一個值變量產生的ABA問題,理解一下ABA問題產生的流程:

@SneakyThrows
@Test
public void test1() {
    AtomicInteger atomicInteger = new AtomicInteger(10);

    CountDownLatch countDownLatch = new CountDownLatch(2);

    new Thread(() -> {
        atomicInteger.compareAndSet(10, 11);
        atomicInteger.compareAndSet(11,10);
        System.out.println(Thread.currentThread().getName() + ":10->11->10");
        countDownLatch.countDown();
    }).start();

    new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
            boolean isSuccess = atomicInteger.compareAndSet(10,12);
            System.out.println("設置是否成功:" + isSuccess + ",設置的新值:" + atomicInteger.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        countDownLatch.countDown();
    }).start();

    countDownLatch.await();
}
//輸出:線程2並無發現初始值已經被修改
//Thread-0:10->11->10
//設置是否成功:true,設置的新值:12

ABA問題存在,但可能對值變量並不會形成結果上的影響,可是考慮一種特殊的狀況:

https://zhuanlan.zhihu.com/p/237611535

  1. 線程1和線程2併發訪問ConcurrentStack。
  2. 線程1執行出棧【預期結果是彈出B,A成爲棧頂】,但在讀取棧頂B以後,被線程2搶佔。
  3. 線程2記錄棧頂B,依次彈出B和A,再依次將C,D,B入棧,且保證B就是原棧頂記錄的B。
  4. 以後輪到線程1,發現棧頂確實是指望的B,遂彈出B,但此時棧頂已是D,就出現了錯誤。

BAB的問題如何解決

AtomicStampedReference 原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,能夠解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

@SneakyThrows
@Test
public void test2() {
    AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(10,1);

    CountDownLatch countDownLatch = new CountDownLatch(2);

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
        atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp());
        atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp());
        countDownLatch.countDown();
    }).start();

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
        try {
            TimeUnit.SECONDS.sleep(2);
            boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 當前版本:" + atomicStampedReference.getStamp() + " 當前值:" + atomicStampedReference.getReference());
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    countDownLatch.await();
}
//輸出
//輸出
Thread-0 第一次版本:1
Thread-0 第二次版本:2
Thread-0 第三次版本:3
Thread-1 第一次版本:3
Thread-1 修改是否成功:true 當前版本:4 當前值:12

而AtomicMarkableReference 經過標誌位,標誌位只有true和false,每次更新標誌位的話,在第三次的時候,又會變得跟第一次同樣,並不能解決ABA問題。

@SneakyThrows
@Test
public void test3() {
    AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(10, false);

    CountDownLatch countDownLatch = new CountDownLatch(2);

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 第一次標記:" + markableReference.isMarked());
        markableReference.compareAndSet(10, 11, markableReference.isMarked(), true);
        System.out.println(Thread.currentThread().getName() + " 第二次標記:" + markableReference.isMarked());
        markableReference.compareAndSet(11, 10, markableReference.isMarked(), false);
        System.out.println(Thread.currentThread().getName() + " 第三次標記:" + markableReference.isMarked());
        countDownLatch.countDown();
    }).start();

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 第一次標記:" + markableReference.isMarked());
        try {
            TimeUnit.SECONDS.sleep(2);
            boolean isSuccess = markableReference.compareAndSet(10,12, false, true);
            System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 當前標記:" + markableReference.isMarked() + " 當前值:" + markableReference.getReference());
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    countDownLatch.await();
}
//輸出
Thread-0 第一次標記:false
Thread-0 第二次標記:true
Thread-0 第三次標記:false
Thread-1 第一次標記:false
Thread-1 修改是否成功:true 當前標記:true 當前值:12

參考

相關文章
相關標籤/搜索