只有光頭才能變強
以前已經寫過多線程相關的文章了,有興趣的同窗能夠去了解一下:java
在閱讀《阿里巴巴 Java開發手冊》讀後感時,還有未解決的問題:git
若是是count++操做,使用以下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);若是是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減小樂觀鎖的重試次數)。
以前在學習的時候也看過AtomicInteger類不少次了,一直沒有去作相關的筆記。如今遇到問題了,因而就過來寫寫筆記,並但願在學習的過程當中解決掉問題。github
首先咱們來個例子:算法
public class AtomicMain { public static void main(String[] args) throws InterruptedException { ExecutorService service = Executors.newCachedThreadPool(); Count count = new Count(); // 100個線程對共享變量進行加1 for (int i = 0; i < 100; i++) { service.execute(() -> count.increase()); } // 等待上述的線程執行完 service.shutdown(); service.awaitTermination(1, TimeUnit.DAYS); System.out.println("公衆號:Java3y---------"); System.out.println(count.getCount()); } } class Count{ // 共享變量 private Integer count = 0; public Integer getCount() { return count; } public void increase() { count++; } }
大家猜猜得出的結果是多少?是100嗎?編程
多運行幾回能夠發現:結果是不肯定的,多是95,也多是98,也多是100數組
根據結果咱們得知:上面的代碼是線程不安全的!若是線程安全的代碼,屢次執行的結果是一致的!安全
咱們能夠發現問題所在:count++
並不是原子操做。由於count++
須要通過讀取-修改-寫入
三個步驟。舉個例子:多線程
count++
,此時count的值爲11count++
,此時count的值也是11(由於線程B讀到的count是10)要將上面的代碼變成線程安全的(每次得出的結果是100),那也很簡單,畢竟咱們是學過synchronized鎖的人:併發
increase()
加synchronized鎖就行了public synchronized void increase() { count++; }
不管執行多少次,得出的都是100:高併發
從上面的代碼咱們也能夠發現,只作一個++
這麼簡單的操做,都用到了synchronized鎖,未免有點小題大作了。
因而咱們原子變量的類就登場了!
在寫文章以前,本覺得對CAS有必定的瞭解了(由於以前已經看過相關概念,覺得本身理解了)..但真正敲起鍵盤寫的時候,仍是發現沒徹底弄懂...因此再來看看CAS吧。
來源維基百科:
比較並交換(compare and swap, CAS),是 原子操做的一種,可用於在多線程編程中實現 不被打斷的數據交換操做,從而避免多線程同時改寫某一數據時因爲執行順序不肯定性以及中斷的不可預知性產生的數據不一致問題。 該操做經過將內存中的值與指定數據進行比較,當數值同樣時將內存中的數據替換爲新的值。
CAS有3個操做數:
當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內存值V相同時,將內存值V修改成B),而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試(或者什麼都不作)。
咱們畫張圖來理解一下:
咱們能夠發現CAS有兩種狀況:
若是內存值V和咱們的預期值A不相等,通常也有兩種狀況:
咱們再繼續往下看,若是內存值V和咱們的預期值A不相等時,應該何時重試,何時什麼都不作。
好比說,我上面用了100個線程,對count值進行加1。咱們都知道:若是在線程安全的狀況下,這個count值最終的結果必定是爲100的。那就意味着:每一個線程都會對這個count值實質地進行加1。
我繼續畫張圖來講明一下CAS是如何重試(循環再試)的:
上面圖只模擬出兩個線程的狀況,但足夠說明問題了。
上面是每一個線程都要爲count值加1,但咱們也能夠有這種狀況:將count值設置爲5
我也來畫個圖說明一下:
理解CAS的核心就是:CAS是原子性的,雖然你可能看到比較後再修改(compare and swap)以爲會有兩個操做,但終究是原子性的!
原子變量類在java.util.concurrent.atomic
包下,整體來看有這麼多個:
咱們能夠對其進行分類:
基本類型:
數組:
引用類型:
對象的屬性:
JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
Atomic包裏的類基本都是使用Unsafe實現的包裝類。
Unsafe裏邊有幾個咱們喜歡的方法(CAS):
// 第一和第二個參數表明對象的實例以及地址,第三個參數表明指望值,第四個參數表明更新值 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
從原理上概述就是:Atomic包的類的實現絕大調用Unsafe的方法,而Unsafe底層其實是調用C代碼,C代碼調用匯編,最後生成出一條CPU指令cmpxchg,完成操做。這也就爲啥CAS是原子性的,由於它是一條CPU指令,不會被打斷。
既然咱們上面也說到了,使用Synchronized鎖有點小題大做了,咱們用原子變量類來改一下:
class Count{ // 共享變量(使用AtomicInteger來替代Synchronized鎖) private AtomicInteger count = new AtomicInteger(0); public Integer getCount() { return count.get(); } public void increase() { count.incrementAndGet(); } } // Main方法仍是如上
修改完,不管執行多少次,咱們的結果永遠是100!
其實Atomic包下原子類的使用方式都不會差太多,瞭解原子類各類類型,看看API,基本就會用了(網上也寫得比較詳細,因此我這裏果斷偷懶了)...
使用CAS有個缺點就是ABA的問題,什麼是ABA問題呢?首先我用文字描述一下:
count=10
,如今有三個線程,分別爲A、B、C上面的操做均可以正常執行完的,這樣會發生什麼問題呢??線程C沒法得知線程A和線程B修改過的count值,這樣是有風險的。
下面我再畫個圖來講明一下ABA的問題(以鏈表爲例):
要解決ABA的問題,咱們可使用JDK給咱們提供的AtomicStampedReference和AtomicMarkableReference類。
AtomicStampedReference:
An {@code AtomicStampedReference} maintains an object referencealong with an integer "stamp", that can be updated atomically.
簡單來講就是在給爲這個對象提供了一個版本,而且這個版本若是被修改了,是自動更新的。
原理大概就是:維護了一個Pair對象,Pair對象存儲咱們的對象引用和一個stamp值。每次CAS比較的是兩個Pair對象
// Pair對象 private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; // 比較的是Pari對象 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
由於多了一個版本號比較,因此就不會存在ABA的問題了。
若是是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減小樂觀鎖的重試次數)。
去查閱了一些博客和資料,大概的意思就是:
而LongAdder能夠歸納成這樣:內部核心數據value分離成一個數組(Cell),每一個線程訪問時,經過哈希等算法映射到其中一個數字進行計數,而最終的計數結果,則爲這個數組的求和累加。
參考資料:
參考資料:
若是你以爲我寫得還不錯,瞭解一下: