Java中的Atomic包使用指南

引言
Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環境下,無鎖的進行原子操做。底層使用了Unsafe類的native方法,提供了硬件級別的原子操做,可是不一樣的CPU架構可能提供的原子指令不同,也有可能須要某種形式的內部鎖,因此該包下的方法不能絕對保證線程不被阻塞。java

Atomic包介紹
在Atomic包裏一共有17個類,四種原子更新方式,分別是原子更新基本類型,原子更新數組,原子更新引用和原子更新字段。Atomic包裏的類基本都是使用Unsafe的CAS實現的包裝類。程序員

原子更新基本類型類
用於經過原子的方式更新基本類型,Atomic包提供瞭如下三個類:數組

AtomicBoolean:原子更新布爾類型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新長整型。
AtomicInteger的經常使用方法以下:安全

int addAndGet(int delta) :以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果
boolean compareAndSet(int expect, int update) :若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
int getAndIncrement():以原子方式將當前值加1,注意:這裏返回的是自增前的值。
void lazySet(int newValue):最終會設置成newValue,使用lazySet設置值後,可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。關於該方法的更多信息能夠參考併發網翻譯的一篇文章《AtomicLong.lazySet是如何工做的?》
int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。
AtomicInteger例子代碼以下:多線程

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    static AtomicInteger ai = new AtomicInteger(1);

    public static void main(String[] args) {
        System.out.println(ai.getAndIncrement());
        System.out.println(ai.get());
    }

}

輸出架構

1
2

餐後甜點
Atomic包提供了三種基本類型的原子更新,可是Java的基本類型裏還有char,float和double等。那麼問題來了,如何原子的更新其餘的基本類型呢?Atomic包裏的類基本都是使用Unsafe實現的,讓咱們一塊兒看下Unsafe的源碼,發現Unsafe只提供了三種CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源碼,發現其是先把Boolean轉換成整型,再使用compareAndSwapInt進行CAS,因此原子更新double也能夠用相似的思路來實現。併發

原子更新數組類
經過原子的方式更新數組裏的某個元素,Atomic包提供瞭如下三個類:函數

AtomicIntegerArray:原子更新整型數組裏的元素。
AtomicLongArray:原子更新長整型數組裏的元素。
AtomicReferenceArray:原子更新引用類型數組裏的元素。
AtomicIntegerArray類主要是提供原子的方式更新數組裏的整型,其經常使用方法以下高併發

int addAndGet(int i, int delta):以原子方式將輸入值與數組中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update):若是當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
實例代碼以下:性能

public class AtomicIntegerArrayTest {

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

    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
        System.out.println(value[0]);
    }

}

輸出

3
1

AtomicIntegerArray類須要注意的是,數組value經過構造方法傳遞進去,而後AtomicIntegerArray會將當前數組複製一份,因此當AtomicIntegerArray對內部的數組元素進行修改時,不會影響到傳入的數組。

原子更新引用類型
原子更新基本類型的AtomicInteger,只能更新一個變量,若是要原子的更新多個變量,就須要使用這個原子更新引用類型提供的類。Atomic包提供瞭如下三個類:

AtomicReference:原子更新引用類型。
AtomicReference的使用例子代碼以下:

import java.util.concurrent.atomic.AtomicReference;
import static java.lang.System.out;

public class AtomicReferenceTest {
    public static void main(String[] args) {
        AtomicReference<User> atomicUserRef = new AtomicReference();
        User user = new User("last", 15);
        atomicUserRef.set(user);
        User updateUser = new User("soul", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        out.printf("name:%s,age:%d \n",atomicUserRef.get().getName(),atomicUserRef.get().getAge());
    }

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

輸出

name:soul,age:17

原子更新字段類
若是咱們只須要某個類裏的某個字段,那麼就須要使用原子更新字段類AtomicReferenceFieldUpdater。
要想使用原子更新字段類須要注意如下問題:

  1. 更新類的屬性必須使用public volatile進行修飾;
  2. 更新類的屬性不能是基本類型,必須是包裝類型或者其餘類型,好比:Integer,String等;
  3. 一個更新器能夠更新同一類型的多個對象;

AtomicReferenceFieldUpdater不能更新原子類型屬性,原子類型屬性要用AtomicIntegerFieldUpdater,它要注意的問題同上,除了第二條對屬性的限制。它要求屬性必須是int,而不能是Integer.
和AtomicIntegerFieldUpdater相似的是AtomicLongFieldUpdater,它們是原子更新基本類型字段的更新器。

示例代碼以下:

public class FieldUpdaterTest {
    public static void main(String[] args) {
        //定義一個更新器,指定更新的對象,字段類型以及字段名字
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
        //要被更新的對象
        User user1 = new User("user1", 1);
        //更新對象的屬性
        updater.compareAndSet(user1, user1.age, 3);
        System.out.println(user1.age);

        //另外一個要被更新的對象
        User user2 = new User("user2", 1);
        //更新對象的屬性
        updater.compareAndSet(user2, user2.age, 4);
        System.out.println(user2.age);

        //Integer類型屬性更新器,它的屬性必須是原子類型,而不能是包裝類型
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
        //要被更新的對象
        User user3 = new User("user3", 1, 4);
        //更新對象的屬性
        integerFieldUpdater.compareAndSet(user3, user3.old, 5);
        System.out.println(user3.old);

    }

    static class User {
        public volatile String userName;
        public volatile Integer age;
        public volatile int old;

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

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

輸出

10
11

AtomicReference經過volatile和Unsafe提供的CAS函數實現原子操做。 自旋+CAS的無鎖操做保證共享變量的線程安全。
可是CAS操做可能存在ABA問題。AtomicStampedReference和AtomicMarkableReference的出現就是爲了解決這問題。
AtomicStampedReference

構造方法中initialStamp(時間戳)用來惟一標識引用變量,在構造器內部,實例化了一個Pair對象,Pair對象記錄了對象引用和時間戳信息,採用int做爲時間戳,實際使用的時候,要保證時間戳惟一(通常作成自增的),若是時間戳若是重複,還會出現ABA的問題。

AtomicStampedReference中的每個引用變量都帶上了pair.stamp這個時間戳,這樣就能夠解決CAS中的ABA的問題。

示例代碼:

public class StampedReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        //初始值,也能夠用自定義對象
        int initialRef = 1;
        //初始版本
        int initialStamp = 1;
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(initialRef, initialStamp);
        System.out.println("initial ref=" + atomicStampedReference.getReference() + " and stamp=" + atomicStampedReference.getStamp());
        //指望值
        final int expectedRef = atomicStampedReference.getReference();
        //新值
        int newRef = atomicStampedReference.getReference() + 10;
        //指望版本
        final int expectedStamp = atomicStampedReference.getStamp();
        //新版本
        int newStamp = atomicStampedReference.getStamp() + 1;
        Thread t1 = new Thread(() -> System.out.println("success:"
                + atomicStampedReference.compareAndSet(expectedRef, newRef, expectedStamp, newStamp)));

        t1.start();
        t1.join();
        System.out.println("ref=" + atomicStampedReference.getReference() + " and stamp=" + atomicStampedReference.getStamp());
        Thread t2 = new Thread(() -> System.out.println("success:"
                + atomicStampedReference.compareAndSet(expectedRef, newRef, expectedStamp, newStamp)));

        t2.start();
        t2.join();
        System.out.println("ref=" + atomicStampedReference.getReference() + " and stamp=" + atomicStampedReference.getStamp());
    }
}

AtomicMarkableReference
AtomicStampedReference能夠知道,引用變量中途被更改了幾回。有時候,咱們並不關心引用變量更改了幾回,只是單純的關心是否更改過,因此就有了AtomicMarkableReference。

AtomicMarkableReference的惟一區別就是再也不用int標識引用,而是使用boolean變量——表示引用變量是否被更改過。

示例代碼

public class AtomicMarkableReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        //初始值,也能夠用自定義對象
        int initialRef = 1;
        //初始標識
        boolean initialMark = false;
        AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference(initialRef, initialMark);
        System.out.println("initial ref=" + markableReference.getReference() + " and isMarked=" + markableReference.isMarked());
        //指望值
        int expectedRef = markableReference.getReference();
        /**
         * 併發過程當中t1,t2兩個線程的指望值都是expectedRef,把值加10
         * t1,t2兩個線程的指望標誌都是false,操做成功後置爲true
         * t1成功後標誌變爲true,因此與t2指望值不符,操做失敗
         */
        Thread t1 = new Thread(() -> System.out.println("success:"
                + markableReference.compareAndSet(expectedRef, markableReference.getReference() + 10, false, true)));

        t1.start();
        t1.join();
        System.out.println("ref=" + markableReference.getReference() + " and isMarked=" + markableReference.isMarked());
        Thread t2 = new Thread(() -> System.out.println("success:"
                + markableReference.compareAndSet(expectedRef, markableReference.getReference() + 10, false, false)));

        t2.start();
        t2.join();
        System.out.println("ref=" + markableReference.getReference() + " and isMarked=" + markableReference.isMarked());
    }    
}

DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder是JDK1.8新增的部分,是對AtomicLong等類的改進。好比LongAccumulator與LongAdder在高併發環境下比AtomicLong更高效。本文以LongAdder爲例,學習這些類。

API中是這麼介紹的:LongAdder中會維護一組(一個或多個)變量,這些變量加起來就是要以原子方式更新的long型變量。當更新方法add(long)在線程間競爭時,該組變量能夠動態增加以減緩競爭。方法sum()返回當前在維持總和的變量上的總和。與AtomicLong相比,LongAdder更多地用於收集統計數據,而不是細粒度的同步控制。在低併發環境下,二者性能很類似。但在高併發環境下,LongAdder有着明顯更高的吞吐量,可是有着更高的空間複雜度。
實現原理
與其餘原子類同樣,LongAdder也是基於CAS實現的。

LongAdder能夠代替AtomicLong嗎

固然不能。在上面已經提到,與AtomicLong相比,LongAdder更多地用於收集統計數據,而不是細粒度的同步控制。並且,LongAdder只提供了add(long)和decrement()方法,想要使用cas方法仍是要選擇AtomicLong。

DoubleAdder、LongAccumulator、DoubleAccumulator與LongAdder很類似,就很少作介紹了。

相關文章
相關標籤/搜索