全面瞭解 Java 原子變量類

📦 本文以及示例源碼已歸檔在 javacorehtml

1、原子變量類簡介

爲什麼須要原子變量類

保證線程安全是 Java 併發編程必需要解決的重要問題。Java 從原子性、可見性、有序性這三大特性入手,確保多線程的數據一致性。java

  • 確保線程安全最多見的作法是利用鎖機制(Locksychronized)來對共享數據作互斥同步,這樣在同一個時刻,只有一個線程能夠執行某個方法或者某個代碼塊,那麼操做必然是原子性的,線程安全的。互斥同步最主要的問題是線程阻塞和喚醒所帶來的性能問題。
  • volatile 是輕量級的鎖(天然比普通鎖性能要好),它保證了共享變量在多線程中的可見性,但沒法保證原子性。因此,它只能在一些特定場景下使用。
  • 爲了兼顧原子性以及鎖帶來的性能問題,Java 引入了 CAS (主要體如今 Unsafe 類)來實現非阻塞同步(也叫樂觀鎖)。並基於 CAS ,提供了一套原子工具類。

原子變量類的做用

原子變量類 比鎖的粒度更細,更輕量級,而且對於在多處理器系統上實現高性能的併發代碼來講是很是關鍵的。原子變量將發生競爭的範圍縮小到單個變量上。git

原子變量類至關於一種泛化的 volatile 變量,可以支持原子的、有條件的讀/改/寫操做。github

原子類在內部使用 CAS 指令(基於硬件的支持)來實現同步。這些指令一般比鎖更快。編程

原子變量類能夠分爲 4 組:數組

  • 基本類型
    • AtomicBoolean - 布爾類型原子類
    • AtomicInteger - 整型原子類
    • AtomicLong - 長整型原子類
  • 引用類型
    • AtomicReference - 引用類型原子類
    • AtomicMarkableReference - 帶有標記位的引用類型原子類
    • AtomicStampedReference - 帶有版本號的引用類型原子類
  • 數組類型
    • AtomicIntegerArray - 整形數組原子類
    • AtomicLongArray - 長整型數組原子類
    • AtomicReferenceArray - 引用類型數組原子類
  • 屬性更新器類型
    • AtomicIntegerFieldUpdater - 整型字段的原子更新器。
    • AtomicLongFieldUpdater - 長整型字段的原子更新器。
    • AtomicReferenceFieldUpdater - 原子更新引用類型裏的字段。

這裏不對 CAS、volatile、互斥同步作深刻探討。若是想了解更多細節,不妨參考:Java 併發核心機制緩存

2、基本類型

這一類型的原子類是針對 Java 基本類型進行操做。安全

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

以上類都支持 CAS,此外,AtomicIntegerAtomicLong 還支持算術運算。多線程

提示:併發

雖然 Java 只提供了 AtomicBooleanAtomicIntegerAtomicLong,可是能夠模擬其餘基本類型的原子變量。要想模擬其餘基本類型的原子變量,能夠將 shortbyte 等類型與 int 類型進行轉換,以及使用 Float.floatToIntBitsDouble.doubleToLongBits 來轉換浮點數。

因爲 AtomicBooleanAtomicIntegerAtomicLong 實現方式、使用方式都相近,因此本文僅針對 AtomicInteger 進行介紹。

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) // 獲取當前值,並加上預期值
boolean compareAndSet(int expect, int update) // 若是輸入值(update)等於預期值,將該值設置爲輸入值
public final void lazySet(int newValue) // 最終設置爲 newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

AtomicInteger 使用示例:

public class AtomicIntegerDemo {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        AtomicInteger count = new AtomicInteger(0);
        for (int i = 0; i < 1000; i++) {
            executorService.submit((Runnable) () -> {
                System.out.println(Thread.currentThread().getName() + " count=" + count.get());
                count.incrementAndGet();
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(30, TimeUnit.SECONDS);
        System.out.println("Final Count is : " + count.get());
    }
}

AtomicInteger 實現

閱讀 AtomicInteger 源碼,能夠看到以下定義:

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

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

private volatile int value;

說明:

  • value - value 屬性使用 volatile 修飾,使得對 value 的修改在併發環境下對全部線程可見。
  • valueOffset - value 屬性的偏移量,經過這個偏移量能夠快速定位到 value 字段,這個是實現 AtomicInteger 的關鍵。
  • unsafe - Unsafe 類型的屬性,它爲 AtomicInteger 提供了 CAS 操做。

3、引用類型

Java 數據類型分爲 基本數據類型引用數據類型 兩大類(不瞭解 Java 數據類型劃分能夠參考: Java 基本數據類型 )。

上一節中提到了針對基本數據類型的原子類,那麼若是想針對引用類型作原子操做怎麼辦?Java 也提供了相關的原子類:

  • AtomicReference - 引用類型原子類
  • AtomicMarkableReference - 帶有標記位的引用類型原子類
  • AtomicStampedReference - 帶有版本號的引用類型原子類

AtomicStampedReference 類在引用類型原子類中,完全地解決了 ABA 問題,其它的 CAS 能力與另外兩個類相近,因此最具表明性。所以,本節只針對 AtomicStampedReference 進行說明。

示例:基於 AtomicReference 實現一個簡單的自旋鎖

public class AtomicReferenceDemo2 {

    private static int ticket = 10;

    public static void main(String[] args) {
        threadSafeDemo();
    }

    private static void threadSafeDemo() {
        SpinLock lock = new SpinLock();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyThread(lock));
        }
        executorService.shutdown();
    }

    /**
     * 基於 {@link AtomicReference} 實現的簡單自旋鎖
     */
    static class SpinLock {

        private AtomicReference<Thread> atomicReference = new AtomicReference<>();

        public void lock() {
            Thread current = Thread.currentThread();
            while (!atomicReference.compareAndSet(null, current)) {}
        }

        public void unlock() {
            Thread current = Thread.currentThread();
            atomicReference.compareAndSet(current, null);
        }

    }

    /**
     * 利用自旋鎖 {@link SpinLock} 併發處理數據
     */
    static class MyThread implements Runnable {

        private SpinLock lock;

        public MyThread(SpinLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            while (ticket > 0) {
                lock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票");
                    ticket--;
                }
                lock.unlock();
            }
        }

    }

}

原子類的實現基於 CAS 機制,而 CAS 存在 ABA 問題(不瞭解 ABA 問題,能夠參考:Java 併發基礎機制 - CAS 的問題)。正是爲了解決 ABA 問題,纔有了 AtomicMarkableReferenceAtomicStampedReference

AtomicMarkableReference 使用一個布爾值做爲標記,修改時在 true / false 之間切換。這種策略不能根本上解決 ABA 問題,可是能夠下降 ABA 發生的概率。經常使用於緩存或者狀態描述這樣的場景。

public class AtomicMarkableReferenceDemo {

    private final static String INIT_TEXT = "abc";

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

        final AtomicMarkableReference<String> amr = new AtomicMarkableReference<>(INIT_TEXT, false);

        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(Math.abs((int) (Math.random() * 100)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    String name = Thread.currentThread().getName();
                    if (amr.compareAndSet(INIT_TEXT, name, amr.isMarked(), !amr.isMarked())) {
                        System.out.println(Thread.currentThread().getName() + " 修改了對象!");
                        System.out.println("新的對象爲:" + amr.getReference());
                    }
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(3, TimeUnit.SECONDS);
    }

}

AtomicStampedReference 使用一個整型值作爲版本號,每次更新前先比較版本號,若是一致,才進行修改。經過這種策略,能夠根本上解決 ABA 問題。

public class AtomicStampedReferenceDemo {

    private final static String INIT_REF = "pool-1-thread-3";

    private final static AtomicStampedReference<String> asr = new AtomicStampedReference<>(INIT_REF, 0);

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

        System.out.println("初始對象爲:" + asr.getReference());

        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            executorService.execute(new MyThread());
        }

        executorService.shutdown();
        executorService.awaitTermination(3, TimeUnit.SECONDS);
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(Math.abs((int) (Math.random() * 100)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            final int stamp = asr.getStamp();
            if (asr.compareAndSet(INIT_REF, Thread.currentThread().getName(), stamp, stamp + 1)) {
                System.out.println(Thread.currentThread().getName() + " 修改了對象!");
                System.out.println("新的對象爲:" + asr.getReference());
            }
        }

    }

}

4、數組類型

Java 提供瞭如下針對數組的原子類:

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

已經有了針對基本類型和引用類型的原子類,爲何還要提供針對數組的原子類呢?

數組類型的原子類爲 數組元素 提供了 volatile 類型的訪問語義,這是普通數組所不具有的特性——volatile 類型的數組僅在數組引用上具備 volatile 語義

示例:AtomicIntegerArray 使用示例(AtomicLongArrayAtomicReferenceArray 使用方式也相似)

public class AtomicIntegerArrayDemo {

    private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);

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

        System.out.println("Init Values: ");
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            atomicIntegerArray.set(i, i);
            System.out.print(atomicIntegerArray.get(i) + " ");
        }
        System.out.println();

        Thread t1 = new Thread(new Increment());
        Thread t2 = new Thread(new Compare());
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final Values: ");
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.print(atomicIntegerArray.get(i) + " ");
        }
        System.out.println();
    }

    static class Increment implements Runnable {

        @Override
        public void run() {

            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                int value = atomicIntegerArray.incrementAndGet(i);
                System.out.println(Thread.currentThread().getName() + ", index = " + i + ", value = " + value);
            }
        }

    }

    static class Compare implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                boolean swapped = atomicIntegerArray.compareAndSet(i, 2, 3);
                if (swapped) {
                    System.out.println(Thread.currentThread().getName() + " swapped, index = " + i + ", value = 3");
                }
            }
        }

    }

}

5、屬性更新器類型

更新器類支持基於反射機制的更新字段值的原子操做。

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

這些類的使用有必定限制:

  • 由於對象的屬性修改類型原子類都是抽象類,因此每次使用都必須使用靜態方法 newUpdater() 建立一個更新器,而且須要設置想要更新的類和屬性。
  • 字段必須是 volatile 類型的;
  • 不能做用於靜態變量(static);
  • 不能做用於常量(final);
public class AtomicReferenceFieldUpdaterDemo {

    static User user = new User("begin");

    static AtomicReferenceFieldUpdater<User, String> updater =
        AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyThread());
        }
        executorService.shutdown();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            if (updater.compareAndSet(user, "begin", "end")) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 已修改 name = " + user.getName());
            } else {
                System.out.println(Thread.currentThread().getName() + " 已被其餘線程修改");
            }
        }

    }

    static class User {

        volatile String name;

        public User(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public User setName(String name) {
            this.name = name;
            return this;
        }

    }

}

參考資料

相關文章
相關標籤/搜索