06-Atomic原子操作類詳解

Atomic原子操作類介紹

在併發編程中很容易出現併發安全的問題,有一個很簡單的例子就是多線程更新變量i=1,比如多個線程執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到線程安全的目的。但是由於synchronized是採用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,在J.U.C下的atomic包提供了一系列的操作簡單,性能高效,並能保證線程安全的類去更新基本類型變量,數組元素,引用類型以及更新對象中的字段類型。atomic包下的這些類都是採用的是樂觀鎖策略去原子更新數據,在java中則是使用CAS操作具體實現。

在java.util.concurrent.atomic包裏提供了一組原子操作類:

基本類型:AtomicInteger、AtomicLong、AtomicBoolean;

引用類型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;

數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

對象屬性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

原子類型累加器(jdk1.8增加的類):DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

原子更新基本類型

AtomicInteger

以AtomicInteger爲例總結常用的方法

//以原子的方式將實例中的原值加1,返回的是自增前的舊值;
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
 
//getAndSet(int newValue):將實例中的值更新爲新值,並返回舊值;
public final boolean getAndSet(boolean newValue) {
    boolean prev;
    do {
        prev = get();
    } while (!compareAndSet(prev, newValue));
    return prev;
}
 
//incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加後的結果;
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
 
//addAndGet(int delta) :以原子方式將輸入的數值與實例中原本的值相加,並返回最後的結果;
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

測試

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    static AtomicInteger sum = new AtomicInteger(0);

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    // 原子自增  CAS
                    sum.incrementAndGet();
                    //count++;

                }
            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sum.get());

    }

}

incrementAndGet()方法通過CAS自增實現,如果CAS失敗,自旋直到成功+1。

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

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;
}

原子更新數組類型

AtomicIntegerArray

AtomicIntegerArray爲例總結常用的方法

//addAndGet(int i, int delta):以原子更新的方式將數組中索引爲i的元素與輸入值相加;
public final int addAndGet(int i, int delta) {
    return getAndAdd(i, delta) + delta;
}
 
//getAndIncrement(int i):以原子更新的方式將數組中索引爲i的元素自增加1;
public final int getAndIncrement(int i) {
    return getAndAdd(i, 1);
}
 
//compareAndSet(int i, int expect, int update):將數組中索引爲i的位置的元素進行更新
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

測試

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest {

    static int[] value = new int[]{ 1, 2, 3, 4, 5 };
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);


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

        //設置索引0的元素爲100
        atomicIntegerArray.set(0, 100);
        System.out.println(atomicIntegerArray.get(0));
        //以原子更新的方式將數組中索引爲1的元素與輸入值相加
        atomicIntegerArray.getAndAdd(1,5);

        System.out.println(atomicIntegerArray);
    }
}

原子更新引用類型

AtomicReference

AtomicReference作用是對普通對象的封裝,它可以保證你在修改對象引用時的線程安全性。

import java.util.concurrent.atomic.AtomicReference;
import lombok.AllArgsConstructor;
import lombok.Data;

public class AtomicReferenceTest {

    public static void main( String[] args ) {
        User user1 = new User("張三", 23);
        User user2 = new User("李四", 25);
        User user3 = new User("王五", 20);

        //初始化爲 user1
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);

        //把 user2 賦給 atomicReference
        atomicReference.compareAndSet(user1, user2);
        System.out.println(atomicReference.get());

        //把 user3 賦給 atomicReference
        atomicReference.compareAndSet(user1, user3);
        System.out.println(atomicReference.get());

    }

}


@Data
@AllArgsConstructor
class User {
    private String name;
    private Integer age;
}

對象屬性原子修改器

AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater可以線程安全地更新對象中的整型變量。

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {


    public static class Candidate {
        //字段必須是volatile類型
        volatile int score = 0;

        AtomicInteger score2 = new AtomicInteger();
    }

    public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
            AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

    public static AtomicInteger realScore = new AtomicInteger(0);

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

        final Candidate candidate = new Candidate();

        Thread[] t = new Thread[10000];
        for (int i = 0; i < 10000; i++) {
            t[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    if (Math.random() > 0.4) {
                        candidate.score2.incrementAndGet();
                        scoreUpdater.incrementAndGet(candidate);
                        realScore.incrementAndGet();
                    }
                }
            });
            t[i].start();
        }
        for (int i = 0; i < 10000; i++) {
            t[i].join();
        }
        System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score);
        System.out.println("AtomicInteger Score=" + candidate.score2.get());
        System.out.println("realScore=" + realScore.get());

    }
}

對於AtomicIntegerFieldUpdater 的使用稍微有一些限制和約束,約束如下:

  • (1)字段必須是volatile類型的,在線程之間共享變量時保證立即可見.eg:volatile int value = 3
  • (2)字段的描述類型(修飾符public/protected/default/private)與調用者與操作對象字段的關係一致。也就是說調用者能夠直接操作對象字段,那麼就可以反射進行原子操作。但是對於父類的字段,子類是不能直接操作的,儘管子類可以訪問父類的字段。
  • (3)只能是實例變量,不能是類變量,也就是說不能加static關鍵字。
  • (4)只能是可修改變量,不能使final變量,因爲final的語義就是不可修改。實際上final的語義和volatile是有衝突的,這兩個關鍵字不能同時存在。
  • (5)對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。如果要修改包裝類型就需要使用AtomicReferenceFieldUpdater。

LongAdder/DoubleAdder詳解

AtomicLong是利用了底層的CAS操作來提供併發性的,比如addAndGet方法:

public final long getAndAdd(long delta) {
        return unsafe.getAndAddLong(this, valueOffset, delta);
    }

public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

上述方法調用了Unsafe類的getAndAddLong方法,該方法內部是個native方法,它的邏輯是採用自旋的方式不斷更新目標值,直到更新成功。

在併發量較低的環境下,線程衝突的概率比較小,自旋的次數不會很多。但是,高併發環境下,N個線程同時進行自旋操作,會出現大量失敗並不斷自旋的情況,此時AtomicLong的自旋會成爲瓶頸。

這就是LongAdder引入的初衷——解決高併發環境下AtomicInteger,AtomicLong的自旋瓶頸問題。

LongAdder

性能測試

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class LongAdderTest {

    public static void main(String[] args) {
        LongAdder longAdder=new LongAdder();
        longAdder.add(1);
        testAtomicLongVSLongAdder(10, 10000);
        System.out.println("==================");
        testAtomicLongVSLongAdder(10, 200000);
        System.out.println("==================");
        testAtomicLongVSLongAdder(100, 200000);
    }

    static void testAtomicLongVSLongAdder(final int threadCount, final int times) {
        try {
            long start = System.currentTimeMillis();
            testLongAdder(threadCount, times);
            long end = System.currentTimeMillis() - start;
            System.out.println("條件>>>>>>線程數:" + threadCount + ", 單線程操作計數" + times);
            System.out.println("結果>>>>>>LongAdder方式增加計數" + (threadCount * times) + "次,共計耗時:" + end);

            long start2 = System.currentTimeMillis();
            testAtomicLong(threadCount, times);
            long end2 = System.currentTimeMillis() - start2;
            System.out.println("條件>>>>>>線程數:" + threadCount + ", 單線程操作計數" + times);
            System.out.println("結果>>>>>>AtomicLong方式增加計數" + (threadCount * times) + "次,共計耗時:" + end2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static void testAtomicLong(final int threadCount, final int times) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        AtomicLong atomicLong = new AtomicLong();
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < times; j++) {
                        atomicLong.incrementAndGet();
                    }
                    countDownLatch.countDown();
                }
            }, "my-thread" + i).start();
        }
        countDownLatch.await();
    }

    static void testLongAdder(final int threadCount, final int times) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < times; j++) {
                        longAdder.add(1);
                    }
                    countDownLatch.countDown();
                }
            }, "my-thread" + i).start();
        }

        countDownLatch.await();
    }
}

測試結果:線程數越多,併發操作數越大,LongAdder的優勢越明顯

條件>>>>>>線程數:10, 單線程操作計數10000
結果>>>>>>LongAdder方式增加計數100000次,共計耗時:10
條件>>>>>>線程數:10, 單線程操作計數10000
結果>>>>>>AtomicLong方式增加計數100000次,共計耗時:5
==================
條件>>>>>>線程數:10, 單線程操作計數200000
結果>>>>>>LongAdder方式增加計數2000000次,共計耗時:18
條件>>>>>>線程數:10, 單線程操作計數200000
結果>>>>>>AtomicLong方式增加計數2000000次,共計耗時:41
==================
條件>>>>>>線程數:100, 單線程操作計數200000
結果>>>>>>LongAdder方式增加計數20000000次,共計耗時:67
條件>>>>>>線程數:100, 單線程操作計數200000
結果>>>>>>AtomicLong方式增加計數20000000次,共計耗時:364

低併發、一般的業務場景下AtomicLong是足夠了。如果併發量很多,存在大量寫多讀少的情況,那LongAdder可能更合適。

LongAdder原理

設計思路

AtomicLong中有個內部變量value保存着實際的long值,所有的操作都是針對該變量進行。也就是說,高併發環境下,value變量其實是一個熱點,也就是N個線程競爭一個熱點。LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。

image

LongAdder的內部結構

LongAdder內部有一個base變量,一個Cell[]數組:

base變量:非競態條件下,直接累加到該變量上

Cell[]數組:競態條件下,累加個各個線程自己的槽Cell[i]中

/** Number of CPUS, to place bound on table size */
// CPU核數,用來決定槽數組的大小
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * Table of cells. When non-null, size is a power of 2.
 */
 // 數組槽,大小爲2的次冪
transient volatile Cell[] cells;

/**
 * Base value, used mainly when there is no contention, but also as
 * a fallback during table initialization races. Updated via CAS.
 */
 /**
 *  基數,在兩種情況下會使用:
 *  1. 沒有遇到併發競爭時,直接使用base累加數值
 *  2. 初始化cells數組時,必須要保證cells數組只能被初始化一次(即只有一個線程能對cells初始化),
 *  其他競爭失敗的線程會講數值累加到base上
 */
transient volatile long base;

/**
 * Spinlock (locked via CAS) used when resizing and/or creating Cells.
 */
 transient volatile int cellsBusy;
Cell

@sun.misc.Contended是jdk提供的註解用於填充緩存行

定義了一個內部Cell類,這就是我們之前所說的槽,每個Cell對象存有一個value值,可以通過Unsafe來CAS操作它的值:

@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}
LongAdder#add方法

LongAdder#add方法的邏輯如下圖:

image

只有從未出現過併發衝突的時候,base基數纔會使用到,一旦出現了併發衝突,之後所有的操作都只針對Cell[]數組中的單元Cell。

如果Cell[]數組未初始化,會調用父類的longAccumelate去初始化Cell[],如果Cell[]已經初始化但是衝突發生在Cell單元內,則也調用父類的longAccumelate,此時可能就需要對Cell[]擴容了。

這也是LongAdder設計的精妙之處:儘量減少熱點衝突,不到最後萬不得已,儘量將CAS操作延遲。

Striped64#longAccumulate方法

整個Striped64#longAccumulate的流程圖如下:

image

LongAdder#sum方法
/**
* 返回累加的和,也就是"當前時刻"的計數值
* 注意: 高併發時,除非全局加鎖,否則得不到程序運行中某個時刻絕對準確的值
*  此返回值可能不是絕對準確的,因爲調用這個方法時還有其他線程可能正在進行計數累加,
*  方法的返回時刻和調用時刻不是同一個點,在有併發的情況下,這個值只是近似準確的計數值
*/
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

由於計算總和時沒有對Cell數組進行加鎖,所以在累加過程中可能有其他線程對Cell中的值進行了修改,也有可能對數組進行了擴容,所以sum返回的值並不是非常精確的,其返回值並不是一個調用sum方法時的原子快照值。

LongAccumulator

LongAccumulator是LongAdder的增強版。LongAdder只能針對數值的進行加減運算,而LongAccumulator提供了自定義的函數操作。其構造函數如下:

public class LongAccumulator extends Striped64 implements Serializable {
    private static final long serialVersionUID = 7249069246863182397L;

    private final LongBinaryOperator function;
    private final long identity;

    /**
     * Creates a new instance using the given accumulator function
     * and identity element.
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @param identity identity (initial value) for the accumulator function
     */
    public LongAccumulator(LongBinaryOperator accumulatorFunction,
                           long identity) {
        this.function = accumulatorFunction;
        base = this.identity = identity;
    }
 
}

通過LongBinaryOperator,可以自定義對入參的任意操作,並返回結果(LongBinaryOperator接收2個long作爲參數,並返回1個long)。

@FunctionalInterface
public interface LongBinaryOperator {

    /**
     * Applies this operator to the given operands.
     *
     * @param left the first operand
     * @param right the second operand
     * @return the operator result
     */
    long applyAsLong(long left, long right);
}

LongAccumulator內部原理和LongAdder幾乎完全一樣,都是利用了父類Striped64的longAccumulate方法。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.stream.IntStream;

public class LongAccumulatorTest {

    public static void main(String[] args) throws InterruptedException {
        // 累加 x+y
        LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);

        ExecutorService executor = Executors.newFixedThreadPool(8);
        // 1到9累加
        IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

        Thread.sleep(2000);
        System.out.println(accumulator.getThenReset());

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