1. 程式人生 > >Java併發程式設計包中atomic的實現原理

Java併發程式設計包中atomic的實現原理

這是一篇來自粉絲的投稿,作者【林灣村龍貓】最近在閱讀Java原始碼,這一篇是他關於併發包中atomic類的原始碼閱讀的總結。Hollis做了一點點修改。

引子

在多執行緒的場景中,我們需要保證資料安全,就會考慮同步的方案,通常會使用synchronized或者lock來處理,使用了synchronized意味著核心態的一次切換。這是一個很重的操作。

有沒有一種方式,可以比較便利的實現一些簡單的資料同步,比如計數器等等。concurrent包下的atomic提供我們這麼一種輕量級的資料同步的選擇。

使用例子

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class App {

    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(100);

        AtomicInteger atomicInteger = new AtomicInteger(0);
        for (int i = 0; i < 100; i++) {
            new Thread() {
                @Override
                public void run() {
                    atomicInteger.getAndIncrement();

                    countDownLatch.countDown();
                }
            }.start();
        }

        countDownLatch.await();

        System.out.println(atomicInteger.get());
    }
}

在以上程式碼中,使用AtomicInteger聲明瞭一個全域性變數,並且在多執行緒中進行自增,程式碼中並沒有進行顯示的加鎖。

以上程式碼的輸出結果,永遠都是100。如果將AtomicInteger換成Integer,列印結果基本都是小於100。

也就說明AtomicInteger宣告的變數,在多執行緒場景中的自增操作是可以保證執行緒安全的。接下來我們分析下其原理。

原理

我們可以看一下AtomicInteger的程式碼

他的值是存在一個volatile的int裡面。volatile只能保證這個變數的可見性。不能保證他的原子性。

可以看看getAndIncrement這個類似i++的函式,可以發現,是呼叫了UnSafe中的getAndAddInt。

UnSafe是何方神聖?UnSafe提供了java可以直接操作底層的能力。

進一步,我們可以發現實現方式:

如何保證原子性:自旋 + CAS(樂觀鎖)。在這個過程中,通過compareAndSwapInt比較更新value值,如果更新失敗,重新獲取舊值,然後更新。

優缺點

CAS相對於其他鎖,不會進行核心態操作,有著一些效能的提升。但同時引入自旋,當鎖競爭較大的時候,自旋次數會增多。cpu資源會消耗很高。

換句話說,CAS+自旋適合使用在低併發有同步資料的應用場景。

Java 8做出的改進和努力

在Java 8中引入了4個新的計數器型別,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator。他們都是繼承於Striped64。

在LongAdder 與AtomicLong有什麼區別? Atomic*遇到的問題是,只能運用於低併發場景。因此LongAddr在這基礎上引入了分段鎖的概念。可以參考《JDK8系列之LongAdder解析》一起看看做了什麼。

大概就是當競爭不激烈的時候,所有執行緒都是通過CAS對同一個變數(Base)進行修改,當競爭激烈的時候,會將根據當前執行緒雜湊到對於Cell上進行修改(多段鎖)。

可以看到大概實現原理是:通過CAS樂觀鎖保證原子性,通過自旋保證當次修改的最終修改成功,通過降低鎖粒度(多段鎖)增加併發效能。