12個原子類基本操作

簡介

官方介紹

當程序更新一個變量時,如果多線程同時更新這個變量,可能得到期望之外的值。
比如變量 i = 1A 線程更新 i+1B 線程也更新i+1,經過兩個線程操作之後可能 i 不等於 3,而是等於 2
因爲 AB 線程在更新變量 i 的時候拿到的 i 都是 1,這就是 線程不安全的更新操作,通常我們會使用 synchronized 來解決這個問題,synchronized 會保證多線程不會同時更新變量 i

而 Java 從 JDK 1.5 開始提供了 java.util.concurrent.atomic 包(以下簡稱Atomic包),這個包中的 原子操作類 提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。

因爲變量的類型有很多種,所以在 Atomic 包裏一共提供了 12個 類,屬於以下 4 種類型的原子更新方式:

  • 原子更新基本類型
    • AtomicBoolean:原子更新布爾類型。
    • AtomicInteger:原子更新整型。
    • AtomicLong:原子更新長整型。
  • 原子更新數組
    • AtomicIntegerArray:原子更新整型數組裏的元素。
    • AtomicLongArray:原子更新長整型數組裏的元素。
    • AtomicReferenceArray:原子更新引用類型數組裏的元素。
  • 原子更新引用
    • AtomicReference:原子更新對象引用。
    • AtomicMarkableReference:原子更新帶有標記位的對象引用。
    • AtomicStampedReference:原子更新帶有版本號的對象引用。
  • 原子更新屬性(字段)
    • AtomicIntegerFieldUpdater:原子更新volatile修飾的整型的字段的更新器。
    • AtomicLongFieldUpdater:原子更新volatile修飾的長整型字段的更新器。
    • AtomicReferenceFieldUpdater:原子更新volatile修飾的引用類型裏的字段的更新器。

Atomic 包裏的類基本都是使用 Unsafe 實現的包裝類。


原子更新基本類型

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

以上3個類提供的方法幾乎一模一樣,所以本節僅以 AtomicInteger 爲例進行講解。

AtomicInteger 的常用方法如下

  • int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger 裏的 value)相加,並返回結果。
  • boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
  • int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。
  • void lazySet(int newValue):最終會設置成 newValue,使用 lazySet 設置值後,可導致其他線程在之後的一小段時間內還是可以讀到舊的值。
  • int getAndSet(int newValue):以原子方式設置爲 newValue 的值,並返回舊值。

示例代碼:

 

public static void main(String[] args) {
    AtomicInteger ai = new AtomicInteger(1);

    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.addAndGet(5) = " + ai.addAndGet(5));
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.compareAndSet(ai.get(), 10) = " + ai.compareAndSet(ai.get(), 10));
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.getAndIncrement() = " + ai.getAndIncrement());
    System.out.println("ai.get() = " + ai.get());

    ai.lazySet(8);
    System.out.println("ai.lazySet(8)");
    System.out.println("ai.get() = " + ai.get());

    System.out.println("ai.getAndSet(5) = " + ai.getAndSet(5));
    System.out.println("ai.get() = " + ai.get());
}

輸出:

 

ai.get() = 1
ai.addAndGet(5) = 6
ai.get() = 6
ai.compareAndSet(ai.get(), 10) = true
ai.get() = 10
ai.getAndIncrement() = 10
ai.get() = 11
ai.lazySet(8)
ai.get() = 8
ai.getAndSet(5) = 8
ai.get() = 5

AtomicIntegergetAndIncrement()方法:

 

public final int getAndIncrement() {
    for (; ; ) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) {
            return current;
        }
    }
}
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • for 循環體的先取得 AtomicInteger 裏存儲的數值
  • AtomicInteger 的當前數值進行 +1 操作,
  • 關鍵是調用 compareAndSet 方法來進行原子更新操作,該方法先檢查 當前數值是否等於current
    • 等於意味着 AtomicInteger 的值沒有被其他線程修改過,則將 AtomicInteger 的當前數值更新成 next的值。
    • 如果不等 compareAndSet 方法會返回 false,程序會進入 for 循環重新進行 compareAndSet 操作。

Atomic 包提供了 3 種基本類型的原子更新,但是 Java 的基本類型裏還有 charfloatdouble 等。
那麼問題來了,如何原子的更新其他的基本類型呢?
Atomic包裏的類基本都是使用 Unsafe 實現的,讓我們一起看一下Unsafe的源碼:

 

/**
 * 如果當前數值是expected,則原子的將Java變量更新成x
 *
 * @return 如果更新成功則返回true
 */
public final native boolean compareAndSwapObject(Object o, long offset,
                                                 Object expected, Object x);

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected, int x);

public final native boolean compareAndSwapLong(Object o, long offset,
                                               long expected, long x);

通過以上代碼,我們發現 Unsafe 只提供了 3 種 CAS 方法:compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong,再看 AtomicBoolean 源碼,發現它是先把 Boolean 轉換成 整型,再使用 compareAndSwapInt 進行 CAS,所以原子更新 charfloatdouble 變量也可以用類似的思路來實現。


原子更新數組

  • AtomicIntegerArray:原子更新整型數組裏的元素。
  • AtomicLongArray:原子更新長整型數組裏的元素。
  • AtomicReferenceArray:原子更新引用類型數組裏的元素。

以上幾個類提供的方法幾乎一樣,所以僅以 AtomicIntegerArray 爲例進行介紹:
AtomicIntegerArray 類主要是提供原子的方式更新數組裏的整型。

常用方法如下:

  • int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。
  • boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

示例代碼:

 

public static void main(String[] args) {

    int[] value = new int[]{1, 2};

    AtomicIntegerArray ai = new AtomicIntegerArray(value);

    System.out.println("ai.getAndSet(0, 3)");
    ai.getAndSet(0, 3);
    System.out.println("ai.get(0) = " + ai.get(0));
    System.out.println("value[0] = " + value[0]);

    ai.compareAndSet(1, 2, 5);
    System.out.println("ai.compareAndSet(1, 2, 5)");
    System.out.println("ai.get(1) = " + ai.get(1));
}

輸出結果:

 

ai.getAndSet(0, 3)
ai.get(0) = 3
value[0] = 1
ai.compareAndSet(1,2,5)
ai.get(1) = 5

需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行 修改 時,不會影響傳入的數組


原子更新引用

  • AtomicReference:原子更新對象引用。
  • AtomicMarkableReference:原子更新帶有標記位的對象引用。
  • AtomicStampedReference:原子更新帶有版本號的對象引用。該類將整數值與引用關聯起來,可用於原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA問題

以上幾個類提供的方法幾乎一樣,所以僅以AtomicReference爲例進行介紹。

示例代碼:

 

public class AtomicReferenceTest {
    public static AtomicReference<User> atomicUserRef = new
            AtomicReference<User>();

    public static void main(String[] args) {
        User user = new User("103style", 20);
        atomicUserRef.set(user);
        System.out.println("atomicUserRef.get() = " + atomicUserRef.get().toString());

        User updateUser = new User("xiaoke", 22);
        atomicUserRef.compareAndSet(user, updateUser);

        System.out.println("atomicUserRef.compareAndSet(user, updateUser);");

        System.out.println("atomicUserRef.get() = " + atomicUserRef.get().toString());
    }

    static class User {
        private String name;
        private int age;

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

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "name='" + name + ", age=" + age;
        }
    }
}

輸出結果:

 

atomicUserRef.get() = name='103style, age=20
atomicUserRef.compareAndSet(user, updateUser);
atomicUserRef.get() = name='xiaoke, age=22

原子更新屬性(字段)

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

要想原子地更新字段類需要兩步

  • 因爲原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性。
  • 更新類的字段(屬性)必須使用public volatile修飾符。

以上3個類提供的方法幾乎一樣,所以僅以 AstomicIntegerFieldUpdater 爲例進行講解。

示例代碼:

 

public class AtomicIntegerFieldUpdaterTest {

    public static void main(String[] args) {
        // 創建原子更新器,並設置需要更新的對象類和對象的屬性
        AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.
                newUpdater(User.class, "age");
        // 設置柯南的年齡是10歲
        User conan = new User("conan", 10);
        // 柯南長了一歲,但是仍然會輸出舊的年齡
        System.out.println(a.getAndIncrement(conan));
        // 輸出柯南現在的年齡
        System.out.println(a.get(conan));
    }

    public static class User {
        public volatile int age;
        private String name;

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

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}

輸出結果:

 

10
11



鏈接:https://www.jianshu.com/p/cb64f79058c2
 

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