Java併發編程之原子變量

     原子變量最主要的一個特點就是所有的操作都是原子的,synchronized關鍵字也可以做到對變量的原子操作。只是synchronized的成本相對較高,需要獲取鎖對象,釋放鎖對象,如果不能獲取到鎖,還需要阻塞在阻塞隊列上進行等待。而如果單單只是爲了解決對變量的原子操作,建議使用原子變量。關於原子變量的介紹,主要涉及以下內容:

  • 原子變量的基本概念
  • 通過AtomicInteger瞭解原子變量的基本使用
  • 通過AtomicInteger瞭解原子變量的基本原理
  • AtomicReference的基本使用
  • 使用FieldUpdater操作非原子變量的字段屬性
  • 經典的ABA問題的解決

一、原子變量的基本概念
     原子變量保證了該變量的所有操作都是原子的,不會因爲多線程的同時訪問而導致髒數據的讀取問題。我們先看一段synchronized關鍵字保證變量原子性的代碼:

public class Counter {
    private int count;

    public synchronized void addCount(){
        this.count++;
    }
}

簡單的count++操作,線程對象首先需要獲取到Counter 類實例的對象鎖,然後完成自增操作,最後釋放對象鎖。整個過程中,無論是獲取鎖還是釋放鎖都是相當消耗成本的,一旦不能獲取到鎖,還需要阻塞當前線程等等。

對於這種情況,我們可以將count變量聲明成原子變量,那麼對於count的自增操作都可以以原子的方式進行,就不存在髒數據的讀取了。Java給我們提供了以下幾種原子類型:

  • AtomicInteger和AtomicIntegerArray:基於Integer類型
  • AtomicBoolean:基於Boolean類型
  • AtomicLong和AtomicLongArray:基於Long類型
  • AtomicReference和AtomicReferenceArray:基於引用類型

在本文的餘下內容中,我們將主要介紹AtomicInteger和AtomicReference兩種類型,AtomicBoolean和AtomicLong的使用和內部實現原理幾乎和AtomicInteger一樣。

二、AtomicInteger的基本使用
     首先看它的兩個構造函數:

private volatile int value;

public AtomicInteger(int initialValue) {
    value = initialValue;
}
public AtomicInteger() {

}

可以看到,我們在通過構造函數構造AtomicInteger原子變量的時候,如果指定一個int的參數,那麼該原子變量的值就會被賦值,否則就是默認的數值0。

也有獲取和設置這個value值的方法:

public final int get()
public final void set(int newValue) 

當然,這兩個方法並不是原子的,所以一般也很少使用,而以下的這些基於原子操作的方法則相對使用的頻繁,至於它們的具體實現是怎樣的,我們將在本文的後續小節中進行簡單的學習。

//基於原子操作,獲取當前原子變量中的值併爲其設置新值
public final int getAndSet(int newValue)
//基於原子操作,比較當前的value是否等於expect,如果是設置爲update並返回true,否則返回false
public final boolean compareAndSet(int expect, int update)
//基於原子操作,獲取當前的value值並自增一
public final int getAndIncrement()
//基於原子操作,獲取當前的value值並自減一
public final int getAndDecrement()
//基於原子操作,獲取當前的value值併爲value加上delta
public final int getAndAdd(int delta)
//還有一些反向的方法,比如:先自增在獲取值的等等

下面我們實現一個計數器的例子,之前我們使用synchronized實現過,現在我們使用原子變量再次實現該問題。

//自定義一個線程類
public class MyThread extends Thread {

    public static AtomicInteger value = new AtomicInteger();

    @Override
    public void run(){
        try {
            Thread.sleep((long) ((Math.random())*100));
            //原子自增
            value.incrementAndGet();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//main函數中啓動100條線程並讓他們啓動
public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i=0;i<100;i++){
        threads[i] = new MyThread();
        threads[i].start();
    }

    for (int j=0;j<100;j++){
        threads[j].join();
    }

    System.out.println("value:"+MyThread.value);
}

多次運行會得到相同的結果:

這裏寫圖片描述

很顯然,使用原子變量要比使用synchronized要簡潔的多並且效率也相對較高。

三、AtomicInteger的內部基本原理
     AtomicInteger的實現原理有點像我們的包裝類,內部主要操作的是value字段,這個字段保存就是原子變量的數值。value字段定義如下:

private volatile int value;

首先value字段被volatile修飾,即不存在內存可見性問題。由於其內部實現原子操作的代碼幾乎類似,我們主要學習下incrementAndGet方法的實現。

在揭露該方法的實現原理之前,我們先看另一個方法:

public final boolean compareAndSet(int expect, int update{
     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

compareAndSet方法又被稱爲CAS,該方法調用unsave的一個compareAndSwapInt方法,這個方法是native,我們看不到源碼,但是我們需要知道該方法完成的一個目標:比較當前原子變量的值是否等於expect,如果是則將其修改爲update並返回true,否則直接返回false。當然,這個操作本身就是原子的,較爲底層的實現。

在jdk1.7之前,我們的incrementAndGet方法是這樣實現的:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

方法體是一個死循環,current獲取到當前原子變量中的值,由於value被修飾volatile,所以不存在內存可見性問題,數據一定是最新的。然後current加一後賦值給next,調用我們的CAS原子操作判斷value是否被別的線程修改過,如果還是原來的值,那麼將next的值賦值給value並返回next,否則重新獲取當前value的值,再次進行判斷,直到操作完成。

incrementAndGet方法的一個很核心的思想是,在加一之前先去看看value的值是多少,真正加的時候再去看一下,如果發現變了,不操作數據,否則爲value加一。

但是在jdk1.8以後,做了一些優化,但是最後還是調用的compareAndSwapInt方法。但基本思想還是沒變。

四、AtomicReference的基本使用
     對於一些自定義類或者字符串等這些引用類型,Java併發包也提供了原子變量的接口支持。AtomicReference內部使用泛型來實現的。

private volatile V value;

public AtomicReference(V initialValue) {
    value = initialValue;
}

public AtomicReference() {

}

有關其他的一些原子方法如下:

//獲取並設置value的值爲newvalue
public final V getAndSet(V newValue) {
    return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}

AtomicReference中少了一些自增自減的操作,但是對於value的修改依然是原子的。

五、使用FieldUpdater操作非原子變量的字段屬性
     FieldUpdater允許我們不必將字段設置爲原子變量,利用反射直接以原子方式操作字段。例如:

//定義一個計數器
public class Counter {
    private volatile  int count;

    public int getCount() {
        return count;
    }

    public void addCount(){
        AtomicIntegerFieldUpdater<Counter> updater  = AtomicIntegerFieldUpdater.newUpdater(Counter.class,"count");
        updater.getAndIncrement(this);
    }
}

然後我們創建一百個線程隨機調用同一個Counter對象的addCount方法,無論運行多少次,結果都是一百。這種方式實現的原子操作,對於被操作的變量不需要被包裝成原子變量,但是卻可以直接以原子方式操作它的數值。

六、經典的ABA問題
     我們的原子變量都依賴一個核心的方法,那就是CAS。這個方法最核心的思想就是,更改變量值之前先獲取該變量當前最新的值,然後在實際更改的時候再次獲取該變量的值,如果沒有被修改,那麼進行更改,否則循環上述操作直至更改操作完成。假如一個線程想要對變量count進行修改,實際操作之前獲取count的值爲A,此時來了一個線程將count值修改爲B,又來一個線程獲取count的值爲B並將count修改爲A,此時第一個線程全然不知道count的值已經被修改兩次了,雖然值還是A,但是實際上數據已經是髒的。

這就是典型的ABA問題,一個解決辦法是,對count的每次操作都記錄下當前的一個時間戳,這樣當我們原子操作count之前,不僅查看count的最新數值,還記錄下該count的時間戳,在實際操作的時候,只有在count的數值和時間戳都沒有被更改的情況之下才完成修改操作。

public static void main(String[] args){
    int count=0;
    int stamp = 1;
    AtomicStampedReference reference = new AtomicStampedReference(count,stamp);
    int next = count++;
    reference.compareAndSet(count, next, stamp, stamp);
}

AtomicStampedReference 的CAS方法要求傳入四個參數,該方法的內部會同時比較count和stamp,只有這兩個值都沒有發生改變的前提下,CAS纔會修改count的值。

上述我們介紹了有關原子變量的最基本內容,最後我們比較下原子變量和synchronized關鍵字的區別。

從思維模式上看,原子變量代表一種樂觀的非阻塞式思維,它假定沒有別人會和我同時操作某個變量,於是在實際修改變量的值的之前不會鎖定該變量,但是修改變量的時候是使用CAS進行的,一旦發現衝突,繼續嘗試直到成功修改該變量。

而synchronized關鍵字則是一種悲觀的阻塞式思維,它認爲所有人都會和我同時來操作某個變量,於是在將要操作該變量之前會加鎖來鎖定該變量,進而繼續操作該變量。

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