(基礎系列)atomic原子變數類的用法、原理和用途

猿青木發表於2017-09-29

前言

在當今科技高速發展的時代,計算機的高速發展早已超越“摩爾定律”。在這個計算機相對廉價的時代,作為開發者操作的機器早已不是單核處理器了,早已進入多核時代,業務早已進入並行執行;開發高併發的程式所要掌握的技能也不再是使用沒有效率的鎖,所幸jdk1.5提供在多執行緒情況下無鎖的進行原子操作,也就是這篇文章將要寫的內容。

什麼是“原子變數類”?

自JDK1.5開始提供了java.util.concurrent.atomic包,方便程式設計師在多執行緒環境下,無鎖的進行原子操作。原子變數的底層使用了處理器提供的原子指令,但是不同的CPU架構可能提供的原子指令不一樣,也有可能需要某種形式的內部鎖,所以該方法不能絕對保證執行緒不被阻塞。- 總的來說就是提供非阻塞的執行緒安全程式設計

原子變數類的簡單用法

在介紹用法前先了解jdk軟體包 java.util.concurrent.atomic 中為我們提供了哪些原子類和方法:

(1)類摘要

描述
AtomicBoolean 可以用原子方式更新的 boolean 值。
AtomicInteger 可以用原子方式更新的 int 值。
AtomicIntegerArray 可以用原子方式更新其元素的 int 陣列。
AtomicIntegerFieldUpdater 基於反射的實用工具,可以對指定類的指定 volatile int 欄位進行原子更新。
AtomicLong 可以用原子方式更新的 long 值。
AtomicLongArray 可以用原子方式更新其元素的 long 陣列。
AtomicLongFieldUpdater 基於反射的實用工具,可以對指定類的指定 volatile long 欄位進行原子更新。
AtomicMarkableReference AtomicMarkableReference 維護帶有標記位的物件引用,可以原子方式對其進行更新。
AtomicReference 可以用原子方式更新的物件引用。
AtomicReferenceArray 可以用原子方式更新其元素的物件引用陣列。
AtomicReferenceFieldUpdater 基於反射的實用工具,可以對指定類的指定 volatile 欄位進行原子更新。
AtomicStampedReference AtomicStampedReference 維護帶有整數“標誌”的物件引用,可以用原子方式對其進行更新。

(2)常用方法摘要

返回型別 方法 描述
boolean compareAndSet(boolean expect, boolean update) 如果當前值 == 預期值,則以原子方式將該值設定為給定的更新值
boolean get() 返回當前值。
void set(boolean newValue) 無條件地設定為給定值。
boolean weakCompareAndSet(boolean expect, boolean update) 如果當前值 == 預期值,則以原子方式將該值設定為給定的更新值。

這裡介紹只列出常用的方法,實際中據原子類不同方法略有變化。如需瞭解更多的方法請檢視“線上文件-jdk-z”

線上文件-jdk-zh

(3)簡單使用示例

示例一:原子更新基本型別類--生成序列號

public class Example1 {

    private final AtomicLong sequenceNumber = new AtomicLong(0);
    public long next() {
        //原子增量方法,執行的是i++,所以需要在獲取一次。
        sequenceNumber.getAndIncrement();
        return sequenceNumber.get();
    }

    public void radixNext(int radix){
        for (;;) {
            long i = sequenceNumber.get();
            // 該方法不一定執行成功,所以用無限迴圈來保證操作始終會執行成功一次。
            boolean suc = sequenceNumber.compareAndSet(i, i + radix);
            if (suc) {
                break;
            }
        }
    }


    public static void main(String[] args) {
        Example1 sequencer = new Example1();

        //生成序列號
        for (int i = 0; i < 10; i++) {
            System.out.println(sequencer.next());
        }

        //生成自定義序列號
        for (int i = 0; i < 10; i++) {
            sequencer.radixNext(3);
            System.out.println(sequencer.sequenceNumber.get());
        }


    }

}複製程式碼

執行結果:

1
2
3
4
5
---------------
8
11
14
17
20複製程式碼

示例二:原子方式更新陣列

public class Example2 {

    static AtomicIntegerArray arr = new AtomicIntegerArray(10);

    public static class AddThread implements Runnable{
        public void run(){
            for(int k=0;k<10000;k++){
                // 以原子方式將索引 i 的元素加 1。
                arr.getAndIncrement(k%arr.length());
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[]ts=new Thread[10];
        //建立10個執行緒
        for(int k=0;k<10;k++){
            ts[k] = new Thread(new AddThread());
        }

        //開啟10個執行緒
        for(int k=0;k<10;k++){
            ts[k].start();
        }

        //等待所有執行緒執行完成
        for(int k=0;k<10;k++){
            ts[k].join();
        }

        //列印最終執行結果
        System.out.println(arr);
    }
}複製程式碼

執行結果:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]複製程式碼

示例三:原子方式更新引用

public class Node {
    private int val;
    private volatile Node left, right;

    private static final AtomicReferenceFieldUpdater leftUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "left");
    private static AtomicReferenceFieldUpdater rightUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "right");

    boolean compareAndSetLeft(Node expect, Node update) {
        return leftUpdater.compareAndSet(this, expect, update);
    }

    public Node() {
        this.left = this.right = null;
    }

    public Node(int val) {
        this.val = val;
        this.left = this.right = null;
    }

    public Node(Node left,Node right) {
        this.left = left;
        this.right = right;
    }


    public static void main(String[] args) {
        Node node = new Node(1);
        node.left = new Node(new Node(2),new Node(3));
        node.right = new Node(new Node(4),new Node(5));
        System.out.println(JSON.toJSON(node));
        node.compareAndSetLeft(node.left,node.right);
        System.out.println(JSON.toJSON(node));
    }



    // get and set ...

}複製程式碼

執行結果:

{"val":1,"left":{"val":0,"left":{"val":2},"right":{"val":3}},"right":{"val":0,"left":{"val":4},"right":{"val":5}}}
{"val":1,"left":{"val":0,"left":{"val":4},"right":{"val":5}},"right":{"val":0,"left":{"val":4},"right":{"val":5}}}複製程式碼

(4)小結

原子訪問和更新的記憶體效果一般遵循以下可變規則中的宣告:

  • get 具有讀取 volatile 變數的記憶體效果。
  • set 具有寫入(分配)volatile 變數的記憶體效果。
    除了允許使用後續(但不是以前的)記憶體操作,其自身不施加帶有普通的非 volatile 寫入的重新排序約束,lazySet 具有寫入(分配)volatile 變數的記憶體效果。在其他使用上下文中,當為 null 時(為了垃圾回收),lazySet 可以應用不會再次訪問的引用。
  • weakCompareAndSet 以原子方式讀取和有條件地寫入變數但不 建立任何 happen-before 排序,因此不提供與除 - weakCompareAndSet 目標外任何變數以前或後續讀取或寫入操作有關的任何保證。
  • compareAndSet 和所有其他的讀取和更新操作(如 getAndIncrement)都有讀取和寫入 volatile 變數的記憶體效果。

原子操作的實現原理

關鍵原始碼:

 // setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();複製程式碼

檢視原始碼發現Atomic包裡的類基本都是使用Unsafe實現的,Unsafe只提供了以下三種CAS方法。

關鍵原始碼:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);複製程式碼

檢視方法不難發現是被native修飾的,即被Native修飾的方法在被呼叫時指向的是一個非java程式碼的具體實現,這個實現可能是其他語言或者作業系統。這裡是藉助C來呼叫CPU底層指令來實現的,具體實現原理請點選下面的“實現原理”。

實現原理

原子物件的用途

原子變數類主要用作各種構造塊,用於實現非阻塞資料結構和相關的基礎結構類。compareAndSet方法不是鎖的常規替換方法。僅當物件的重要更新限定於單個變數時才應用它。

例:多執行緒高併發計數器

總結 (摘自網上)

原子變數類相對於基於鎖的版本有幾個效能優勢。首先,它用硬體的原生形態代替 JVM 的鎖定程式碼路徑,從而在更細的粒度層次上(獨立的記憶體位置)進行同步,失敗的執行緒也可以立即重試,而不會被掛起後重新排程。更細的粒度降低了爭用的機會,不用重新排程就能重試的能力也降低了爭用的成本。即使有少量失敗的 CAS 操作,這種方法仍然會比由於鎖爭用造成的重新排程快得多。

相關文章