死磕 java併發包之AtomicInteger源碼分析

問題

(1)什麼是原子操做?java

(2)原子操做和數據庫的ACID有啥關係?數據庫

(3)AtomicInteger是怎麼實現原子操做的?多線程

(4)AtomicInteger是有什麼缺點?併發

簡介

AtomicInteger是java併發包下面提供的原子類,主要操做的是int類型的整型,經過調用底層Unsafe的CAS等方法實現原子操做。源碼分析

還記得Unsafe嗎?點擊連接直達【死磕 java魔法類之Unsafe解析this

原子操做

原子操做是指不會被線程調度機制打斷的操做,這種操做一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。線程

原子操做能夠是一個步驟,也能夠是多個操做步驟,可是其順序不能夠被打亂,也不能夠被切割而只執行其中的一部分,將整個操做視做一個總體是原子性的核心特徵。debug

咱們這裏說的原子操做與數據庫ACID中的原子性,筆者認爲最大區別在於,數據庫中的原子性主要運用在事務中,一個事務以內的全部更新操做要麼都成功,要麼都失敗,事務是有回滾機制的,而咱們這裏說的原子操做是沒有回滾的,這是最大的區別。code

源碼分析

主要屬性

// 獲取Unsafe的實例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 標識value字段的偏移量
private static final long valueOffset;
// 靜態代碼塊,經過unsafe獲取value的偏移量
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
// 存儲int類型值的地方,使用volatile修飾
private volatile int value;

(1)使用int類型的value存儲值,且使用volatile修飾,volatile主要是保證可見性,即一個線程修改對另外一個線程當即可見,主要的實現原理是內存屏障,這裏不展開來說,有興趣的能夠自行查閱相關資料。對象

(2)調用Unsafe的objectFieldOffset()方法獲取value字段在類中的偏移量,用於後面CAS操做時使用。

compareAndSet()方法

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Unsafe中的方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

調用Unsafe.compareAndSwapInt()方法實現,這個方法有四個參數:

(1)操做的對象;

(2)對象中字段的偏移量;

(3)原來的值,即指望的值;

(4)要修改的值;

能夠看到,這是一個native方法,底層是使用C/C++寫的,主要是調用CPU的CAS指令來實現,它可以保證只有當對應偏移量處的字段值是指望值時才更新,即相似下面這樣的兩步操做:

if(value == expect) {
    value = newValue;
}

經過CPU的CAS指令能夠保證這兩步操做是一個總體,也就不會出現多線程環境中可能比較的時候value值是a,而到真正賦值的時候value值可能已經變成b了的問題。

getAndIncrement()方法

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe中的方法
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

getAndIncrement()方法底層是調用的Unsafe的getAndAddInt()方法,這個方法有三個參數:

(1)操做的對象;

(2)對象中字段的偏移量;

(3)要增長的值;

查看Unsafe的getAndAddInt()方法的源碼,能夠看到它是先獲取當前的值,而後再調用compareAndSwapInt()嘗試更新對應偏移量處的值,若是成功了就跳出循環,若是不成功就再從新嘗試,直到成功爲止,這可不就是(CAS+自旋)的樂觀鎖機制麼^^

AtomicInteger中的其它方法幾乎都是相似的,最終會調用到Unsafe的compareAndSwapInt()來保證對value值更新的原子性。

總結

(1)AtomicInteger中維護了一個使用volatile修飾的變量value,保證可見性;

(2)AtomicInteger中的主要方法最終幾乎都會調用到Unsafe的compareAndSwapInt()方法保證對變量修改的原子性。

彩蛋

(1)爲何須要AtomicInteger?

讓咱們來看一個例子:

public class AtomicIntegerTest {
    private static int count = 0;

    public static void increment() {
        count++;
    }

    public static void main(String[] args) {
        IntStream.range(0, 100)
                .forEach(i->
                        new Thread(()->IntStream.range(0, 1000)
                                .forEach(j->increment())).start());

        // 這裏使用2或者1看本身的機器
        // 我這裏是用run跑大於2纔會退出循環
        // 可是用debug跑大於1就會退出循環了
        while (Thread.activeCount() > 1) {
            // 讓出CPU
            Thread.yield();
        }

        System.out.println(count);
    }
}

這裏起了100個線程,每一個線程對count自增1000次,你會發現每次運行的結果都不同,但它們有個共同點就是都不到100000次,因此直接使用int是有問題的。

那麼,使用volatile能解決這個問題嗎?

private static volatile int count = 0;

public static void increment() {
    count++;
}

答案是很遺憾的,volatile沒法解決這個問題,由於volatile僅有兩個做用:

(1)保證可見性,即一個線程對變量的修改另外一個線程當即可見;

(2)禁止指令重排序;

這裏有個很重要的問題,count++其實是兩步操做,第一步是獲取count的值,第二步是對它的值加1。

使用volatile是沒法保證這兩步不被其它線程調度打斷的,因此沒法保證原子性。

這就引出了咱們今天講的AtomicInteger,它的自增調用的是Unsafe的CAS並使用自旋保證必定會成功,它能夠保證兩步操做的原子性。

public class AtomicIntegerTest {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet();
    }

    public static void main(String[] args) {
        IntStream.range(0, 100)
                .forEach(i->
                        new Thread(()->IntStream.range(0, 1000)
                                .forEach(j->increment())).start());

        // 這裏使用2或者1看本身的機器
        // 我這裏是用run跑大於2纔會退出循環
        // 可是用debug跑大於1就會退出循環了
        while (Thread.activeCount() > 1) {
            // 讓出CPU
            Thread.yield();
        }

        System.out.println(count);
    }
}

這裏老是會打印出100000。

(2)說了那麼多,你知道AtomicInteger有什麼缺點嗎?

固然就是著名的ABA問題啦,咱們下章接着聊^^


歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。

qrcode

相關文章
相關標籤/搜索