JUC包中原子類使用及其原理

轉載自:
https://blog.csdn.net/timheath/article/details/71441008
收益匪淺,感謝!


前言

我在詳解JUC之原子類概述這篇文章中介紹了一下原子操作和JUC包下的原子類的類型,現在我就來介紹一下這些原子類。

操作基本類型的原子類

操作基本類型的原子類有3個

  • AtomicInteger:操作int類型
  • AtomicLong:操作long類型
  • AtomicBoolean:操作boolean類型

這些操作基本類型數據的原子類的使用是非常簡單的,你對基本類型數據的操作,在這些相應的類中可以找到一些相應的方法。比如說的i++++i,就分別對應着getAndIncrement()incrementAndGet()方法,還有像加法如i = i + 3就對應着addAndGet(int delta)方法。

這3個類的操作是類似的,只要你看得懂英文一眼就知道它們的方法是做什麼的。需要再次強調的是,這些方法都是原子操作,類似於同步方法,但是沒有使用synchronized關鍵字,也沒有鎖什麼的,而是用CAS+volatile關鍵字實現原子操作的,關於這點,在後面我會介紹一下原子類的實現。如果還不知道原子操作是什麼,我建議你先去看詳解JUC之原子類概述,而且裏面有原子類解決多線程安全問題的例子,在這裏就不重複展示了。

下面我就簡單地演示一下操作基本類型的原子類的一些方法

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.Test;

public class Demo {

    @Test
    public void testAtomicInteger() {
        AtomicInteger i = new AtomicInteger(100);
        int val = i.getAndIncrement();
        System.out.println(val);// 100
        System.out.println(i.intValue());// 101

        val = i.addAndGet(9);
        System.out.println(val);// 110
    }

    @Test
    public void testAtomicLong() {
        AtomicLong l = new AtomicLong(100);
        long val = l.getAndIncrement();
        System.out.println(val);// 100
        System.out.println(l.intValue());// 101

        val = l.addAndGet(9);
        System.out.println(val);// 110
    }

    @Test
    public void testAtomicBoolean(){
        AtomicBoolean b = new AtomicBoolean(true);
        boolean val = b.getAndSet(false);
        System.out.println(val);//true
        val = b.get();
        System.out.println(val);//false
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

實現原理

大家觀察這些原子類,比如說AtomicInteger,它的名字包含了一個Integer,那爲什麼我還要說這是操作基本類型的呢,這明顯是包裝類呀。別急,點進AtomicInteger的源碼看看你就知道,它裏面持有一個int類型變量value這個你一定要記住,因爲它的所有操作都是圍繞着它的

private volatile int value;
  • 1

同理AtomicLongAtomicBoolean也持有它們各自的對應的基本類型變量value,不行你就去看它們的源代碼咯。還一點需要注意的是它們都被volatile關鍵字修飾了

在講解它們的實現前,我們先要了解CAS和volatile是什麼鬼,我就不在這詳細講這兩個東西了,不然篇幅又要老長了,關於CAS可以看Java中CAS詳解,關於volatile可以看java中volatile關鍵字的含義

鑑於它們3個類的實現差不多,所以我就以AtomicInteger類來講解。

我就以int addAndGet(int delta)方法開始,這個方法的意思就是加一個數並獲取結果,比如說value是3而delta是5則返回8,下面是這個方法的源碼

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看到只有一行代碼,這其中有個很重要的東西就是unsafe,這個是sun.misc.Unsafe類型的實例,現在一定注意,這個Unsafe類在JUC包中被頻繁使用到的,所以特別重要。

sun.misc.unsafe

這個類可以用來在任意內存地址位置處讀寫數據,另外,還支持一些CAS原子操作。可見,對於普通用戶來說,使用起來還是比較危險的,一般只是給開發Java API的用,所以人家將它保護地好好的呢,不耍點手段是用不着的了,如果你真想用可以參看sun.misc.Unsafe類的使用

嚴格上來說Unsafe類它不是Java SE中真正的一部份,所以你儘管去Java官方的JDK API文檔找,找得到算我輸。雖然在Oracle那我沒找到,不過我在DocJar: Search Open Source Java API找到了(記住這個網站,搜索Java API很方便)。

這裏就是它的API文檔http://www.docjar.com/docs/api/sun/misc/Unsafe.html,查看它裏面的一些方法,你就可以發現裏面有很多本地(native)方法。

到了這裏我覺得沒必要再深入專研下去了,人家都這麼保護這個Unsafe類,我們還是別戳破它吧,知道它是幹嘛的就好了。

Java 7和Java 8的AtomicInteger源碼區別

再回到我們的AtomicInteger源碼,爲了方便看我再把int addAndGet(int delta)方法貼上來,可以看見這個方法中的唯一一句代碼是unsafe調用了方法getAndAddInt並將方法的返回值(先get再add說明返回的是value,就像i++先使用i值再進行i+1)加上delta得到的值作爲最終返回結果,看方法名我們就很容易理解它到底幹了什麼。

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

相應的int getAndAdd(int delta)你也應該能看懂,這個更簡單,直接調用unsafegetAndAddInt方法,不就是先獲取value值作爲返回結果,再對valuedelta並將結果賦值回給value

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the previous value
 */
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

經過我上面的介紹你就應該知道我們是無法Unsafe的源碼的,所以它的方法源碼我們看不到。不過這樣子會不會覺得很沒意思,說好的源碼分析呢。行,下面就給你看看在Java 7的AtomicInteger源碼,下面是addAndGet方法

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    for (;;) {
        //獲取value值賦值給current
        int current = get();
        //將current值(這個可能不會等於value值,因爲可能value值已經被其它線程修改)加上delta並賦值給next
        int next = current + delta;
        //比較並設置成功的話就返回結果next,不然又會再次循環
        if (compareAndSet(current, next))
            return next;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

吶吶吶,是不是多了幾行代碼,下面我就來解析一下這個方法

(1) 首先第一步獲取value值,並賦值給currentget方法代碼如下,只是“簡單”地返回value

/**
 * Gets the current value.
 *
 * @return the current value
 */
public final int get() {
    return value;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這個其實不簡單,還記得我說過valuevolatile關鍵字修飾了嘛,說明此時get方法獲取到的value一定是最新的。

(2) 然後第二步是,將current加上delta並賦值給next,這裏需要注意的是,此時的current可能不會等於value,因爲value可能已經被其它線程修改。

(3)然後第三步就是最重要的一步,我們先來看一下compareAndSet方法

/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return true if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到這個方法用到了UnsafecompareAndSwapInt函數,看名字就知道它是一個CAS函數了。

這個方法的作用就是,如果當前AtomicInteger持有的value值等於expect,則將value值設置成update,並返回true,不然就什麼都不做返回false。

用大白話來說就是,誒,value你被改過沒有呀,沒有的話我就把你變成update

再接下來,如果compareAndSet方法返回的是true,說明value並沒有被其它線程所修改,說此時操作的結果是正確的,即current的值還是等於value的,所以current + delta的結果就是我們想要的,所以直接返回結果。如果是false話,二話不說開始下一次嘗試,別忘記for (;;)喲。

至於爲什麼要用for (;;)而不是while(true),你可以看看爲什麼JDK源碼中,無限循環大多使用for(;;)而不是while(true)?

關於自旋和樂觀策略

關於上面addAndGet的實現,我們可以發現裏面有個無限循環,不停地去嘗試修改操作,直到修改成功,這就是自旋,是一種樂觀的策略。就是我假設每次執行都沒人跟我爭,有人跟我爭那我就再多嘗試一次。而悲觀的策略就是我認爲一定會有人跟我爭的,所以每次我要執行代碼的時候我先關上門(上鎖)不給別人進。

自旋操作呢有一個比較大的缺點就是,在併發程度高的情況下,可能一個線程嘗試了好久都沒成功,那你想咯,一個線程沒有放棄CPU執行權在那裏不斷循環,不是很浪費CPU資源嗎?

我在後續的文章還會介紹JUC中的鎖(Lock),裏面有一個概念叫自旋鎖。Java中的鎖有很多講究,很有特點,你可以看Java鎖的膨脹過程和優化 瞭解一下

總結操作基本類型的原子類

總結一下操作基本類型的原子類,它們的內部都維護者一個對應的基本類型的成員變量value,這個變量是被volatile關鍵字修飾的,這可以保證每次一個線程要使用它都會拿到最新的值。然後在進行一些原子操作的時候,需要依賴一個神祕人物Unsafe類(這傢伙是一個隱藏很深的東西,我們只需要知道它是幹嘛的就好了),這個類裏面就有一些CAS函數,一些原子操作就是通過自旋方式,不斷地使用CAS函數進行嘗試直到達到自己的目的。

關於操作基本類型的原子類我就介紹到這裏,可能你會覺得我介紹的不全,不過我覺得是夠了,因爲無論是在方法使用方面還是實現方面我都介紹了一兩個典型的例子,而其它的也就是以此類推,都可以看懂的了,看不懂你找我。

其它原子類

至於操作其它類型的原子類,老實說,我本來想每種類型都寫篇文章介紹一下他們的使用和實現的,不過我感覺很沒必要,因爲它們都是換湯不換藥的,實現的套路都差不多,可能操作的類型不同所以功能不太同,但是看API的話你肯定可以知道它是幹嘛的,比如說操作整型數組的原子類java.util.concurrent.atomic.AtomicIntegerArray有個方法int getAndSet(int i, int newValue)方法,看名字就能猜到,先獲取索引爲i的元素再將索引i的元素設置爲newValue,真的是沒什麼好講的。

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