原子更新基本型別類

翻身碼農把歌唱發表於2018-10-07

文章基於jdk1.7,通過學習《Java併發程式設計的藝術》,對Java原子操作的理解

當程式更新一個變數時,如果多執行緒同時更新這個變數。可能得到期望之外的值,比如變數 i=1,A執行緒更新 i+1,B執行緒也更新 i+1,最後得到的可能不是3,而是2。這是因為執行緒A和B在更新變數 i 的時候 拿到的 i 都是 1,這就是執行緒不安全的操作,通常我們會使用synchronized 來解決這個問題,synchronized 會保證多執行緒不會同時更新變數 i 。

而Java從JDK 1.5開始提供了java.util.concurrent.atomic包,這個包中的原子操作類提供了一種用法簡單、效能高效、執行緒安全地一個變數的方式。

因為變數的型別有很多種,所以Atomic包裡一共提供了13個類,屬於4中型別的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性(欄位)。Atomic包裡面的類基本都是使用Unsafe實現的包裝類。

使用原子的方式更新基本型別,Atomic包提供了以下3個類。

  • AtomicBoolean:原子更新布林型別
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新長整型

以上3個類提供的方法幾乎一樣。此處僅以AtomicInteger為例進行說明,其常用方法如下:

  • int addAndGet(int dalta):以原子方式見剛輸入的數值與例項中的值(AtomicIntger裡的value)相加,並返回結果。
  • boolean compareAndSet( int expect, int update):如果輸入的數值等於期望值,則以原子方式將該值設定為輸入的值。
  • int getAndIncrement():以原子方式將當前值加1,注意,這裡返回的是自增前的值。
  • void lazySet(int newValue):最終會設定成newValue,使用lazySet設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
  • int getAndSet( int newValue):以原子方式設定為newValue的值,並返回舊值。
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author: xbq
 * @date: 2018/10/6 18:31
 * @description: 
 */
public class AtomicIntegerTest {

    static AtomicInteger atomicInteger = new AtomicInteger(2);

    public static void main(String[] args) {
        // addAndGet 以原子方式將輸入的數值與例項中的數值相加,並返回結果
        System.out.println(">>0>>>" + atomicInteger.addAndGet(1));
        // 如果輸入的數值等於預期值,則以原子方式將該值設定為輸入的值
        System.out.println(">>1>>>" + atomicInteger.compareAndSet(3, 8080));
        // 獲取值
        System.out.println(">>2>>>" + atomicInteger.get());
        // 以原子方式將當前值加1,返回的值 為自增前的值
        System.out.println(">>3>>>" + atomicInteger.getAndIncrement());
        // 獲取值
        System.out.println(">>4>>>" + atomicInteger.get());
        // 以原子方式將當前值加1,返回的值 為自增後的值
        System.out.println(">>5>>>" + atomicInteger.incrementAndGet());
        // 獲取值
        System.out.println(">>6>>>" + atomicInteger.get());
        // 以原子方式設定為newValue的值,並返回舊值
        System.out.println(">>7>>>" + atomicInteger.getAndSet(86));
        // 獲取值
        System.out.println(">>8>>>" + atomicInteger.get());
        // 最終會設定成newValue,使用lazySet設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值
        atomicInteger.lazySet(10);
        System.out.println(">>9>>>" + atomicInteger.get());
    }
}
複製程式碼

輸出結果如下:

>>0>>>3
>>1>>>true
>>2>>>8080
>>3>>>8080
>>4>>>8081
>>5>>>8082
>>6>>>8082
>>7>>>8082
>>8>>>86
>>9>>>10
複製程式碼

在JDK1.7中,AtomicInteger的getAndIncrement是這樣的 :

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);
}
複製程式碼

而在JDK1.8中,是這樣的:

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

可以看出,在JDK1.7中, 依靠的是我們熟悉的CAS演算法,首先先迴圈獲取當前值,然後對當前值加1 得到新值,對 當前值和新值進行比較並替換,成功的話返回當前值。而在JDK1.8中,直接使用了Unsafe的getAndAddInt 方法,在JDK1.7的Unsafe中,沒有此方法。 JDK1.8中是對CAS演算法的增強。

在Java的基本型別中除了Atomic包中提供原子更新的基本型別外,還有char、float和double。那麼這些在Atomic包中沒有提供原子更新的基本型別怎麼保證其原子更新呢? 從AtomicBoolean原始碼中我們可以得到答案:首先將Boolean轉換為整型,然後使用comareAndSwapInt進行CAS,所以原子更新char、float、double同樣可以以此實現。

歡迎關注我的公眾號~ 搜尋公眾號: 翻身碼農把歌唱 或者 掃描下方二維碼:

img

相關文章