天天看點

通俗有趣講解Atomic原子類的實作原理線程安全真的是線程的安全嗎?什麼是 Atomic?實作一個計數器AtomicInteger 源碼分析AtomicLong 和 LongAdder 誰更牛?總結

  • 線程安全真的是線程的安全嗎?
  • 什麼是 Atomic?
  • 實作一個計數器
  • AtomicInteger 源碼分析
  • AtomicLong 和 LongAdder 誰更牛?
  • 總結

當我們談論『線程安全』的時候,肯定都會想到 Atomic 類。不錯,Atomic 相關類都是線程安全的,在講 Atomic 類之前我想再聊聊『線程安全』這個概念。

線程安全真的是線程的安全嗎?

初看『線程安全』這幾個字,很容易望文生義,這不就是線程的安全嗎?其實不是,線程本身沒有好壞,沒有『安全的線程』和『不安全的線程』之分,俗話說:人之初性本善,線程天生也是純潔善良的,真正讓線程變壞是因為通路的變量的原因,變量對于作業系統來說其實就是記憶體塊,是以繞了這麼一大圈,線程安全稱為『記憶體的安全』可能更為貼切。

簡而言之,線程通路的記憶體決定了這個線程是否是安全的。

變量大緻可以分為局部變量和共享變量,局部變量對于 JVM 來說是棧空間,大家都背過八股文,棧是線程私有的是非共享的,那自然也是記憶體安全的;共享變量對于 JVM 來說一般是存在于堆上,堆上的東西是所有線程共享的,如果不加任何限制自然是不安全的。

因為線程安全這個概念已經深入人心了,是以後面我們還是用線程安全來表達記憶體安全的含義。

那如何解決這種

不安全

呢?方法有很多,比如:加鎖、Atomic 原子類等。

好了,咱們今天先來看看

Atomic類

什麼是 Atomic?

Java

JDK1.5

開始提供

java.util.concurrent.atomic

包,這裡包含了多個原子操作類。原子操作類提供了一個簡單、高效、安全的方式去更新一個變量。

通俗有趣講解Atomic原子類的實作原理線程安全真的是線程的安全嗎?什麼是 Atomic?實作一個計數器AtomicInteger 源碼分析AtomicLong 和 LongAdder 誰更牛?總結

Atomic 包下的原子操作類有很多,可以大緻分為四種類型:

  • 原子操作基本類型
  • 原子操作數組類型
  • 原子操作引用類型
  • 原子操作更新屬性

Atomic原子操作類在源碼中都使用了

Unsafe類

Unsafe類

提供了硬體級别的原子操作,可以安全地直接操作記憶體變量。後面講解源碼時再詳細介紹。

實作一個計數器

假如在業務代碼中需要實作一個計數器的功能,啪地一下,很快我們就寫出了以下的代碼:

publicclass Counter {
    privateint count;

    public void increase() {
        count++;
    }
}
           

increase

方法對 count 變量進行遞增。

當代碼送出上庫進行

code review

時,啪地一下,很快收到了檢視意見(嚴重級别):

如果在多線程場景下,你的計數器可能有問題。

上大一的時候老師就講過 

count++

 是非原子性的,它實際上包含了三個操作:讀資料,加一,寫回資料。

再次修改代碼,多線通路

increase方法

會有問題,那就給它加個鎖吧,count變量修改了其他線程可能不能即時看到,那就給變量加個 

volatile

 吧。

吭哧吭哧,代碼如下:

publicclass LockCounter {
    privatevolatileint count;

    public synchronized void increase() {
        count++;
    }
}
           

一頓操作猛如虎,再次送出代碼後,依然收到了檢視意見(建議級别):

加鎖會影響效率,可以考慮使用原子操作類。

原子操作類?「黑人問号臉」,莫不是大佬知道我晚上有約會故意整我,不想合入代碼吧。帶着将信将疑的态度,打開百度谷歌,原來 AtomicInteger 可以輕松解決這個問題,手忙腳亂一頓複制粘貼代碼搞定了,終于可以下班了。

publicclass AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increase() {
        count.incrementAndGet();
    }
}
           

AtomicInteger 源碼分析

調用

AtomicInteger類

incrementAndGet方法

不用加鎖可以實作安全的遞增,這個好神奇,下面帶領大家分析一下源碼是這麼實作的,等不及了等不及了。

打開源碼,可以看到定義的incrementAndGet方法:

/**
* 在目前值的基礎上自動加 1
*
* @return 更新後的值
*/
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
           

通過源碼可以看到實際上是調用了 unsafe 的一個方法,unsafe 是什麼待會再說。

我們再看看getAndAddInt方法的參數:第一個參數 this 是目前對象的引用;第二個參數valueOffset是用來記錄value值在記憶體中的偏移位址,第三個參數是一個常量 1;

在 AtomicInteger 中定義了一個常量

valueOffset

和一個可變的成員變量 

value

privatestaticfinal Unsafe unsafe = Unsafe.getUnsafe();
privatestaticfinallong valueOffset;

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

privatevolatileint value;
           

value

 變量儲存目前對象的值,

valueOffset

 是變量的記憶體偏移位址,也是通過調用unsafe的方法擷取。

publicfinalclass Unsafe {
    // ……省略其他方法

    public native long objectFieldOffset(Field f);
}
           

這裡再說說 

Unsafe

 這個類,人如其名:不安全的類。打開 Unsafe 類會看到大部分方法都辨別了 

native

,也就是說這些都是本地方法,本地方法強依賴于作業系統平台,一般都是采用

C/C++

語言編寫,在調用 Unsafe 類的本地方法實際會執行這些方法,熟悉 C/C++的小夥伴可自行下載下傳源碼研究。

好了,我們再回到最開始,調用了 Unsafe 類的getAndAddInt方法:

publicfinalclass Unsafe {
    // ……省略其他方法

    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset); 
            // 循環 CAS 操作
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

    // 根據記憶體偏移位址擷取目前值
    public native int getIntVolatile(Object o, long offset);

    // CAS 操作
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
}
           

通過getIntVolatile方法擷取目前 AtomicInteger 對象的value值,這是一個本地方法。

然後調用compareAndSwapInt進行 CAS 原子操作,嘗試在目前值的基礎上加 1,如果 CAS 失敗會循環進行重試。

是以compareAndSwapInt方法是最核心的,詳細實作大家可以自行找源碼看。這裡我們看看方法的參數,一共有四個參數:o 是指目前對象;offset 是指目前對象值的記憶體偏移位址;expected是期望值;x是修改後的值;

compareAndSwapInt方法的思路是拿到對象 o 和 offset 後會再去取對象實際的值,如果目前值與之前取的期望值是一緻的就認為 value 沒有被修改過,直接将 value 的值更新為 x,這樣就完成了一次 CAS 操作,CAS 操作是通過作業系統保證原子性的。

如果目前值與期望值不一緻,說明 value 值被修改過,那麼就會重試 CAS 操作直到成功。

通俗有趣講解Atomic原子類的實作原理線程安全真的是線程的安全嗎?什麼是 Atomic?實作一個計數器AtomicInteger 源碼分析AtomicLong 和 LongAdder 誰更牛?總結

AtomicInteger類中還有很多其他的方法,如:

decrementAndGet()
getAndDecrement()
getAndIncrement()
accumulateAndGet()
// …… 省略
           

這些方法實作原理都是大同小異,希望大家可以舉一反三了解其他的方法。

另外還有一些其他的類,如:

AtomicLong

AtomicReference

AtomicIntegerArray

等,這裡也不再贅述,原理都是大同小異。

AtomicLong 和 LongAdder 誰更牛?

Java 在 

jdk1.8版本

 引入了 

LongAdder

 類,與 

AtomicLong

 一樣可以實作加、減、遞增、遞減等線程安全操作,但是在高并發競争非常激烈的場景下 

LongAdder

 的效率更勝一籌,後續單獨用一篇文章進行介紹。

總結

講了半天,可能有的小夥伴還是比較懵,Atomic 類到底是如何實作線程安全的?

在語言層面上,Atomic 類是沒有做任何同步操作的,翻看源代碼方法沒有任何加鎖,其實最大功勞還是在 CAS 身上。CAS 利用作業系統的硬體特性實作了原子性,利用 CPU 多核能力實作了硬體層面的阻塞。

隻有 CAS 的原子性保證就一定是線程安全的嗎?當然不是的,通過源碼發現 value 變量還用了 volatile 修飾了,保證了線程可見性。

那有些小夥伴可能要問了,那是不是加鎖就沒有用了,非也,雖然基于 CAS 的線程安全機制很好很高效,但是這适合一些粒度比較小的需求才有效,如果遇到非常複雜的業務邏輯還是需要加鎖操作的。

大家學會了嗎?

繼續閱讀