Java中atomic包中的原子操作類總結

你聽___發表於2018-05-06

Java中atomic包中的原子操作類總結

1. 原子操作類介紹

在併發程式設計中很容易出現併發安全的問題,有一個很簡單的例子就是多執行緒更新變數i=1,比如多個執行緒執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到執行緒安全的目的(關於synchronized可以看這篇文章)。但是由於synchronized是採用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,在J.U.C下的atomic包提供了一系列的操作簡單,效能高效,並能保證執行緒安全的類去更新基本型別變數,陣列元素,引用型別以及更新物件中的欄位型別。atomic包下的這些類都是採用的是樂觀鎖策略去原子更新資料,在java中則是使用CAS操作具體實現。

2. 預備知識--CAS操作

能夠弄懂atomic包下這些原子操作類的實現原理,就要先明白什麼是CAS操作。

什麼是CAS?

使用鎖時,執行緒獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區程式碼都會產生衝突,所以當前執行緒獲取到鎖的時候同時也會阻塞其他執行緒獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設所有執行緒訪問共享資源的時候不會出現衝突,既然不會出現衝突自然而然就不會阻塞其他執行緒的操作。因此,執行緒就不會出現阻塞停頓的狀態。那麼,如果出現衝突了怎麼辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑑別執行緒是否出現衝突,出現衝突就重試當前操作直到沒有衝突為止。

CAS的操作過程

CAS比較交換的過程可以通俗的理解為CAS(V,O,N),包含三個值分別為:V 記憶體地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和記憶體中實際的值相同表明該值沒有被其他執行緒更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經被其他執行緒改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個執行緒使用CAS操作一個變數是,只有一個執行緒會成功,併成功更新,其餘會失敗。失敗的執行緒會重新嘗試,當然也可以選擇掛起執行緒

CAS的實現需要硬體指令集的支撐,在JDK1.5後虛擬機器才可以使用處理器提供的CMPXCHG指令實現。

Synchronized VS CAS

元老級的Synchronized(未優化前)最主要的問題是:在存線上程競爭的情況下會出現執行緒阻塞和喚醒鎖帶來的效能問題,因為這是一種互斥同步(阻塞同步)。而CAS並不是武斷的間執行緒掛起,當CAS操作失敗後會進行一定的嘗試,而非進行耗時的掛起喚醒的操作,因此也叫做非阻塞同步。這是兩者主要的區別。

CAS的問題

  1. ABA問題 因為CAS會檢查舊值有沒有變化,這裡存在這樣一個有意思的問題。比如一箇舊值A變為了成B,然後再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化。解決方案可以沿襲資料庫中常用的樂觀鎖方式,新增一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。

  2. 自旋時間過長

使用CAS時非阻塞同步,也就是說不會將執行緒掛起,會自旋(無非就是一個死迴圈)進行下一次嘗試,如果這裡自旋時間過長對效能是很大的消耗。如果JVM能支援處理器提供的pause指令,那麼在效率上會有一定的提升。

3. 原子更新基本型別

atomic包提高原子更新基本型別的工具類,主要有這些:

  1. AtomicBoolean:以原子更新的方式更新boolean;
  2. AtomicInteger:以原子更新的方式更新Integer;
  3. AtomicLong:以原子更新的方式更新Long;

這幾個類的用法基本一致,這裡以AtomicInteger為例總結常用的方法

  1. addAndGet(int delta) :以原子方式將輸入的數值與例項中原本的值相加,並返回最後的結果;
  2. incrementAndGet() :以原子的方式將例項中的原值進行加1操作,並返回最終相加後的結果;
  3. getAndSet(int newValue):將例項中的值更新為新值,並返回舊值;
  4. getAndIncrement():以原子的方式將例項中的原值加1,返回的是自增前的舊值;

還有一些方法,可以檢視API,不再贅述。為了能夠弄懂AtomicInteger的實現原理,以getAndIncrement方法為例,來看下原始碼:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製程式碼

可以看出,該方法實際上是呼叫了unsafe例項的getAndAddInt方法,unsafe例項的獲取時通過UnSafe類的靜態方法getUnsafe獲取:

private static final Unsafe unsafe = Unsafe.getUnsafe();
複製程式碼

Unsafe類在sun.misc包下,Unsafer類提供了一些底層操作,atomic包下的原子操作類的也主要是通過Unsafe類提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法來進行實現。下面用一個簡單的例子來說明AtomicInteger的用法:

public class AtomicDemo {
    private static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.get());
    }
}
輸出結果:
1
2
複製程式碼

例子很簡單,就是新建了一個atomicInteger物件,而atomicInteger的構造方法也就是傳入一個基本型別資料即可,對其進行了封裝。對基本變數的操作比如自增,自減,相加,更新等操作,atomicInteger也提供了相應的方法進行這些操作。但是,因為atomicInteger藉助了UnSafe提供的CAS操作能夠保證資料更新的時候是執行緒安全的,並且由於CAS是採用樂觀鎖策略,因此,這種資料更新的方法也具有高效性。

AtomicLong的實現原理和AtomicInteger一致,只不過一個針對的是long變數,一個針對的是int變數。而boolean變數的更新類AtomicBoolean類是怎樣實現更新的呢?核心方法是compareAndSett方法,其原始碼如下:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
複製程式碼

可以看出,compareAndSet方法的實際上也是先轉換成0,1的整型變數,然後是通過針對int型變數的原子更新方法compareAndSwapInt來實現的。可以看出atomic包中只提供了對boolean,int ,long這三種基本型別的原子更新的方法,參考對boolean更新的方式,原子更新char,doule,float也可以採用類似的思路進行實現。

4. 原子更新陣列型別

atomic包下提供能原子更新陣列中元素的類有:

  1. AtomicIntegerArray:原子更新整型陣列中的元素;
  2. AtomicLongArray:原子更新長整型陣列中的元素;
  3. AtomicReferenceArray:原子更新引用型別陣列中的元素

這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法:

  1. addAndGet(int i, int delta):以原子更新的方式將陣列中索引為i的元素與輸入值相加;
  2. getAndIncrement(int i):以原子更新的方式將陣列中索引為i的元素自增加1;
  3. compareAndSet(int i, int expect, int update):將陣列中索引為i的位置的元素進行更新

可以看出,AtomicIntegerArray與AtomicInteger的方法基本一致,只不過在AtomicIntegerArray的方法中會多一個指定陣列索引位i。下面舉一個簡單的例子:

public class AtomicDemo {
    //    private static AtomicInteger atomicInteger = new AtomicInteger(1);
    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對陣列中索引為1的位置的元素加5
        int result = integerArray.getAndAdd(1, 5);
        System.out.println(integerArray.get(1));
        System.out.println(result);
    }
}
輸出結果:
7
2
複製程式碼

通過getAndAdd方法將位置為1的元素加5,從結果可以看出索引為1的元素變成了7,該方法返回的也是相加之前的數為2。

5. 原子更新引用型別

如果需要原子更新引用型別變數的話,為了保證執行緒安全,atomic也提供了相關的類:

  1. AtomicReference:原子更新引用型別;
  2. AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位;
  3. AtomicMarkableReference:原子更新帶有標記位的引用型別;

這幾個類的使用方法也是基本一樣的,以AtomicReference為例,來說明這些類的基本用法。下面是一個demo

public class AtomicDemo {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

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

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

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

輸出結果:
User{userName='a', age=1}
User{userName='b', age=2}
複製程式碼

首先將物件User1用AtomicReference進行封裝,然後呼叫getAndSet方法,從結果可以看出,該方法會原子更新引用的user物件,變為User{userName='b', age=2},返回的是原來的user物件User{userName='a', age=1}

6. 原子更新欄位型別

如果需要更新物件的某個欄位,並在多執行緒的情況下,能夠保證執行緒安全,atomic同樣也提供了相應的原子操作類:

  1. AtomicIntegeFieldUpdater:原子更新整型欄位類;
  2. AtomicLongFieldUpdater:原子更新長整型欄位類;
  3. AtomicStampedReference:原子更新引用型別,這種更新方式會帶有版本號。而為什麼在更新的時候會帶有版本號,是為了解決CAS的ABA問題;

要想使用原子更新欄位需要兩步操作:

  1. 原子更新欄位類都是抽象類,只能通過靜態方法newUpdater來建立一個更新器,並且需要設定想要更新的類和屬性;
  2. 更新類的屬性必須使用public volatile進行修飾;

這幾個類提供的方法基本一致,以AtomicIntegerFieldUpdater為例來看看具體的使用:

public class AtomicDemo {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    public static void main(String[] args) {
        User user = new User("a", 1);
        int oldValue = updater.getAndAdd(user, 5);
        System.out.println(oldValue);
        System.out.println(updater.get(user));
    }

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

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

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

輸出結果:
1
6
複製程式碼

從示例中可以看出,建立AtomicIntegerFieldUpdater是通過它提供的靜態方法進行建立,getAndAdd方法會將指定的欄位加上輸入的值,並且返回相加之前的值。user物件中age欄位原值為1,加5之後,可以看出user物件中的age欄位的值已經變成了6。

相關文章