java Atomic 基本資料型別

安兒IT發表於2020-11-01

前言

本文講解如下三個基本型別的原子幫助類。

  1. AtomicInteger
  2. AtomicBoolean
  3. AtomicLong

由於三個類所提供的函式都差不多我們以AtomicLong為例進行說明。

我們首先看下我們為何需要原子幫助類。

  1. Long型別寫操作不是原子性,會分為兩個32位進行寫入(讀取是原子性,可以檢視網上相關文獻)。

  2. ++i操作非原子性

  3. 多執行緒可見性問題

  4. 多執行緒資料競爭相關問題,如併發修改

其中舉例如下:

在這裡插入圖片描述
假設 業務邏輯A業務邏輯B只能執行一個,但是在上訴程式碼中有可能同時執行 業務邏輯A業務邏輯B

案例二:

 	
public class Main {

    //火車票剩餘張數
    static int i = 100;
    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            while (i >0) {
                --i;
                System.out.println("我搶到第"+i +"張火車票 執行緒"+Thread.currentThread().getName());
            }
        };
        List<Thread> collect = IntStream.range(0, 100).mapToObj((i) -> {
            Thread thread = new Thread(r);
            return thread;
        }).collect(Collectors.toList());

        collect.forEach(Thread::start);

        for (Thread thread : collect) {
            thread.join();
        }


        System.out.println(i);
    }
}

上面的例子模擬多執行緒搶火車票,但是你會發現多個執行緒會輸出相同序號的火車票(volatile不提供原子性)。

Atomic提供的類函式全部為原子性操作,原子性操作的理解:

int i=0;
i++;

我們仔細看下i++這個步驟其實可用分為如下幾步驟:

1. 獲取i變數的記憶體空間
2. 設定i變數的數值
3. 將新數值放入記憶體

所以在多執行緒進行++i或者i++時往往會出現各種數值無法對應的情況(不考慮記憶體可見性)。而原子類提供類原子性操作,可以把上面三步合成一個彙編指令,當然這是利用CPU提供的指令集完成的。

++i非原子性問題圖:
在這裡插入圖片描述
上圖顯示兩個執行緒同時進行++i的結果,我們預期i應該為58,可是卻為57

由於存在大量的輪子博主不在做過多的解釋,可參考如下文獻
i++位元組碼分析文章

綜上為了規避這些問題我們使用JDK提供的原子類幫助我們快速規避這類問題,

常用方法

我們首先預覽如下JDK提供的公開函式
在這裡插入圖片描述

我們首先直接看一個使用案例在講解全部API


    //火車票剩餘張數
    static AtomicLong atomicLong = new AtomicLong(100);

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            long andIncrement = 0;
            //返回當前數值後 自身減一
            while ((andIncrement = atomicLong.getAndDecrement()) > 0) {
                System.out.println("我搶到第" + andIncrement + "張火車票 執行緒" + Thread.currentThread().getName());
            }
        };
        List<Thread> collect = IntStream.range(0, 100).mapToObj((i) -> {
            Thread thread = new Thread(r);
            return thread;
        }).collect(Collectors.toList());
        //啟動所有執行緒	
        collect.forEach(Thread::start);
        //等候所有執行緒結束
        for (Thread thread : collect) {
            thread.join();
        }

    }

上面的程式碼執行後不會出現多個執行緒搶到同一序號的火車票。

  1. get()
    volatile記憶體語義得到當前數值。(提供可見性,和happen-before)

  2. set(Long)
    volatile記憶體語義設定當前數值。(提供可見性,和happen-before)

  3. lazySet(Long)
    等價 set(Long)沒有實際懶設定,讀者可自行翻譯原始碼

  4. getAndSet(Long)
    設定給定的數值然後返回上一次數值

  5. compareAndSet(long expect, long update)
    著名的CAS操作系列,如果當前值等於expect,那麼更新數值為update,如果更新成功返回true,失敗返回false

  6. weakCompareAndSet(long expect, long update)

    完全等價compareAndSet(long expect, long update),原始碼並沒有做額外的處理

  7. getAndIncrement()

    返回之前的數值然後自增1,可以替換i++等操作

  8. 返回之前的數值然後自身減少1,可以替換--i等操作

    返回之前的數值然後自增1,可以替換i++等操作

  9. getAndAdd(long delta)
    得到之前的數值,然後累加傳入的數值

  10. incrementAndGet()
    先自增再返回自增後的數值

  11. decrementAndGet()
    先自減在返回自減後的數值

  12. addAndGet(long delta)
    先加指定數值再返回加後的數值

  13. getAndUpdate(LongUnaryOperator updateFunction)

    原子性更新LongUnaryOperator介面返回的數值,直到成功才返回之前的數值,如果期間一直競爭那麼此函式會一直阻塞競爭成功。可以簡單理解一個死迴圈加上compareAndSet函式。

    如下案例:


    //火車票剩餘張數
    static AtomicLong atomicLong = new AtomicLong(100);

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

        long andUpdate = atomicLong.getAndUpdate(operand -> operand+1);
        //返回100
        System.out.println(andUpdate);
        //返回101
        System.out.println(atomicLong.get());

    }

這個可以用在搶火車票的案例中。由於getAndUpdate比較晦澀,所以我們檢視下原始碼:

//AtomicLong.java
 public final long getAndUpdate(LongUnaryOperator updateFunction) {
        long prev, next;
        do {
        	//得到之前的數值
            prev = get();
            //呼叫開發傳入的介面函式,介面返回一個新數值
            next = updateFunction.applyAsLong(prev);
            //死迴圈一直到成功為止
        } while (!compareAndSet(prev, next));
        return prev;
    }
  1. updateAndGet(LongUnaryOperator updateFunction)
    getAndUpdate(LongUnaryOperator updateFunction)差不多,只不過返回值是進行計算之後的數值。

  2. getAndAccumulate(long x, LongBinaryOperator accumulatorFunction)
    一直死迴圈到LongBinaryOperator返回的數值被設定成功,返回值為設定之前的數值。

  long andUpdate = atomicLong.getAndAccumulate(2, new LongBinaryOperator() {
            @Override
            public long applyAsLong(long left, long right) {
                return left+right;
            }
        });

        //返回100
        System.out.println(andUpdate);
        //返回102
        System.out.println(atomicLong.get());

  1. accumulateAndGet(long x, LongBinaryOperator accumulatorFunction)
    getAndAccumulate功能一樣,區別在返回值為更新後的數值

相關文章