開發與維運

詳解java併發原子類AtomicInteger(基於jdk1.8源碼分析)

一、從a++說起為什麼使用AtomicInteger


我們知道java併發機制中主要有三個特性需要我們去考慮,原子性、可見性和有序性。volatile關鍵字可以保證可見性和有序性卻無法保證原子性。而這個AtomicInteger的作用就是為了保證原子性。我們先看一個例子。

public class Test {
    //一個變量a
    private static volatile int a = 0;
    public static void main(String[] args) {
        Test test = new Test();
        Thread[] threads = new Thread[5];
        //定義5個線程,每個線程加10
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        System.out.println(a++);
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

在上面的這個例子中,我們定義了一個變量a。並且使用了5個線程分別去增加。為了保證可見性和有序性我們使用了volatile關鍵字對a進行修飾。在這裡我們只測試原子性。如果我們第一次接觸的話肯定會覺得5個線程,每個線程加10,最後結果一定是50呀。我們可以運行一邊測試一波。

v2-2cdc805ca406ef8d61b8a3b7c6161d6d_1440w.jpg

很明顯,可能跟你想象的不一樣。為什麼會出現這個問題呢?這是因為變量a雖然保證了可見性和有序性,但是缺沒有保證原子性。其原因我們可以來分析一下。

對於a++的操作,其實可以分解為3個步驟。

(1)從主存中讀取a的值

(2)對a進行加1操作

(3)把a重新刷新到主存

這三個步驟在單線程中一點問題都沒有,但是到了多線程就出現了問題了。比如說有的線程已經把a進行了加1操作,但是還沒來得及重新刷入到主存,其他的線程就重新讀取了舊值。因為才造成了錯誤。如何去解決呢?方法當然很多,但是為了和我們今天的主題對應上,很自然的聯想到使用AtomicInteger。下面我們使用AtomicInteger重新來測試一遍:

public class Test3 {
    //使用AtomicInteger定義a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        Test3 test = new Test3();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函數進行自增操作
                        System.out.println(a.incrementAndGet());        
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

在上面的代碼中我們使用了AtomicInteger來定義a,而且使用了AtomicInteger的函數incrementAndGet來對a進行自增操作。現在我們再來測試一遍。

v2-07c85ebd9632478169bb1f4ade07eef9_1440w.jpg

現在使用了AtomicInteger,不管你測試多少次,最後結果一定是50。為什麼會出現這樣的結果呢?AtomicInteger又是如何保證了這樣的特性呢?下面我們就正式的開始揭開其面紗。

二、原理分析


上面的例子中我們只是調用了incrementAndGet函數來進行自增操作。其實AtomicInteger類為我們提供了很多函數。可以先使用一下。

1、基本使用


public class Test4 {
    private static AtomicInteger atomicInteger = new AtomicInteger();
    //1、獲取當前值
    public static void getCurrentValue(){}
    //2、設置value值
    public static void setValue(){}
    //3、先獲取舊值,然後設置新值
    public static void getAndSet(){}
    //4、先取得舊值,然後再進行自增
    public static void getAndIncrement(){}
    //5、先獲取舊值,然後再減少
    public static void getAndDecrement(){}
    //6、先獲取舊值,然後再加10
    public static void getAndAdd(){}
    //7、先加1.然後獲取新值
    public static void incrementAndGet(){}
    //8、先減1,然後獲取新值
    public static void decrementAndGet(){}  、
    //9、先增加,然後再獲取新值
    public static void addAndGet(){}
}

最常用的方法就是這麼幾個。當然了還有很多其他的方法。對於上面幾個函數,每一個函數的意思都已經列了出來。意思都很簡單。下面我們就通過源碼的角度分析一下AtomicInteger的真正原理。

2、源碼分析


既然AtomicInteger使用了incrementAndGet函數,那我們就直接來看這個方法,對於其他的方法也是同樣的道理。我們直接看源碼,這裡使用的是jdk1.8的版本,不同的版本會有出入。

/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在這裡我們會看到,底層使用的是unsafe的getAndAddInt方法。這裡你可能有一個疑問了,這個unsafe是個什麼鬼,而且還有一個valueOffset參數又是什麼,想要看明白,我們從源碼的開頭開始看起。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    //這裡還有更多的代碼沒有列出    
}

開頭在Unsafe的上面會發現,有一行註釋叫做Unsafe.compareAndSwapInt。這又是什麼?帶著這些疑問我們開始一點一點揭開其面紗。

(1)compareAndSwapInt的含義


compareAndSwapInt又叫做CAS,如果你將來找工作,這個不清楚的話,基本上可以告別java這個方向了。

CAS 即比較並替換,實現併發算法時常用到的一種技術。CAS操作包含三個操作數——內存位置、預期原值及新值。執行CAS操作的時候,將內存位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新為新值,否則,處理器不做任何操作。

我看過無數篇文章,對這個概念都是這樣解釋的,但是一開始看會一臉懵逼。我們使用一個例子來解釋相信你會更加的清楚。

比如說給你兒子訂婚。你兒子就是內存位置,你原本以為你兒子是和楊貴妃在一起了,結果在訂婚的時候發現兒子身邊是西施。這時候該怎麼辦呢?你一氣之下不做任何操作。如果兒子身邊是你預想的楊貴妃,你一看很開心就給他們訂婚了,也叫作執行操作。現在你應該明白了吧。

對於CAS的解釋我不準備長篇大論講解。因為裡面涉及到的知識點還是挺多的。在這裡你理解了其含義就好。

(2)Unsafe的含義


在上面我們主要是講解了CAS的含義,CAS修飾在Unsafe上面。那這個Unsafe是什麼意思呢?

Unsafe是位於sun.misc包下的一個類,Unsafe類使Java語言擁有了類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語言變得不再“安全”,因此對Unsafe的使用一定要慎重。

這裡說一句題外話,在jdk1.9中,對Usafe進行了刪除,所以因為這,那些基於Usafe開發的框架慢慢的都死掉了。

在這裡也就是說,Usafe再進行getAndAddInt的時候,首先是先加1,然後對底層對象的地址做出了更改。這個地址是什麼呢?這就是涉及到我們的第三個疑問參數了。


(3)valueOffset的含義


這個valueOffset是long類型的,代表的含義就是對象的地址的偏移量。下面我們重新解釋一下這行代碼。

unsafe.getAndAddInt(this, valueOffset, 1) + 1。這行代碼的含義是,usafe通過getAndAddInt方法,對原先對象的地址進行了加1操作。現在應該明白了。我們return的時候,也是直接返回的最新的值。這一點我們對比另外一個方法incrementAndGet就能看出。

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

在這個方法的源代碼中我們可以看到最後的+1操作沒有了,也就是說,直接返回的是舊地址的值,然後再進行自增操作。如何去拿的地址的偏移量呢?是通過下面這個代碼。

static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

OK,到了這一步相信你已經知道了,usafe對a的值使用getAndAddInt方法進行了加1操作。然後返回最新的值。那麼這個getAndAddInt方法是如何實現的呢?我們可以在進入看看:

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

這段代碼的含義也很清晰。底層還是通過compareAndSwapInt這個CAS機制來完成的增加操作,

第一個參數var1表示的是當前對象,也就是a。

第二個參數var2表示的是地址偏移量

第三個參數var3表示的是我們要增加的值,這裡表示為1


對於AtomicInteger的原理就是這,主要是通過Usafe的方式來完成的。Usafe又是通過CAS機制來實現的,因此想要弄清整個原子系列的真正實現,就是要搞清楚CAS機制。不過我會在下一章節進行講解。

3、其他方法


對於其他方法其實也是同樣的道理,我們可以給出幾個看看。

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

我們可以看到底層基本上還是Usafe來實現的。Usafe又是經過CAS實現。

三、總結


對於jdk1.8的併發包來說,底層基本上就是通過Usafe和CAS機制來實現的。有好處也肯定有一個壞處。從好的方面來講,就是上面AtomicInteger類可以保持其原子性。但是從壞的方面來看,Usafe因為直接操作的底層地址,肯定不是那麼安全,而且CAS機制也伴隨著大量的問題,比如說有名的ABA問題等等。關於CAS機制,我也會在後續的文章中專門講解。大家可以先根據那個給兒子訂婚的例子有一個基本的認識。

Leave a Reply

Your email address will not be published. Required fields are marked *