成神之路,需要耐得住寂寞,開啟總結原始碼之旅。
我閱讀總結原始碼的目的不是為了炫技,我希望通過閱讀原始碼可以解決一些問題,也可以通過閱讀原始碼理解別人思想,以幫助我們更好的寫我們的程式碼。
引子
在多執行緒的場景中,我們需要如何同步資料,通常會使用synchronized或者lock來處理,使用了synchronized意味著核心態的一次切換。這是一個很重的操作。有沒有一種方式,可以比較便利的實現一些簡單的資料同步,比如計數器等等。concurrent包下的atomic提供我們這麼一種輕量級的資料同步的選擇。
他山之石
- 說一說Java的Unsafe類:www.cnblogs.com/pkufork/p/j…
- java魔法之unsafe:leokongwq.github.io/2016/12/31/…
- java樂觀鎖實現案例:blog.csdn.net/zhangdehua6…
- Java併發問題--樂觀鎖與悲觀鎖以及樂觀鎖的一種實現方式-CAS:www.cnblogs.com/qjjazry/p/6…
- JDK8系列之LongAdder解析:www.jianshu.com/p/ec045c38e…
- jdk1.8 LongAdder原始碼學習:blog.csdn.net/u011392897/…
使用例子
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());
}
}
複製程式碼
請暫時先忽略CountDownLatch,只是為了在主執行緒中等待所有子執行緒執行完,列印結果。這個結果永遠都是100。如果將AtomicInteger換成Integer,列印結果基本都是小於100。
原理
我們可以看一下AtomicInteger的程式碼
他的值是存在一個volatile的int裡面。volatile只能保證這個變數的可見性。不能保證他的原子性。可以看看getAndIncrement這個類似i++的函式,可以發現,是呼叫了UnSafe中的getAndAddInt。
UnSafe是何方神聖?可以參考上面的文章瞭解一下,UnSafe提供了java可以直接操作底層的能力。 進一步,我們可以發現實現方式:
如何保證原子性:自旋 + CAS(樂觀鎖)。在這個過程中,通過compareAndSwapInt比較更新value值,如果更新失敗,重新獲取舊值,然後更新。
優缺點
CAS相對於其他鎖,不會進行核心態操作,有著一些效能的提升。但同時引入自旋,當鎖競爭較大的時候,自旋次數會增多。cpu資源會消耗很高。
換句話說,CAS+自旋適合使用在低併發有同步資料的應用場景。
jdk8做出的改進和努力
在jdk8中引入了4個新的計數器型別,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator。他們都是繼承於Striped64。
在LongAdder 與AtomicLong有什麼區別? Atomic*遇到的問題是,只能運用於低併發場景。因此LongAddr在這基礎上引入了分段鎖的概念。可以參考《JDK8系列之LongAdder解析》一起看看做了什麼。
大概就是當競爭不激烈的時候,所有執行緒都是通過CAS對同一個變數(Base)進行修改,當競爭激烈的時候,會將根據當前執行緒雜湊到對於Cell上進行修改(多段鎖)。
可以看到大概實現原理是:通過CAS樂觀鎖保證原子性,通過自旋保證當次修改的最終修改成功,通過**降低鎖粒度(多段鎖)**增加併發效能。
關鍵點
- 自旋
- CAS樂觀鎖
- 多段鎖(分治思想)