java原子類AtomicStampedReference

一、什麼是CAS
CAS,compare and swap的縮寫,中文翻譯成比較並交換。
CAS 操作包含三個操作數,內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。
二、案例
public static int count = 0;
private final static int MAX_TREAD = 10;
public static AtomicInteger atomicInteger = new AtomicInteger(0);

/*CountDownLatch能夠使一個線程在等待另外一些線程完成各自工作之後,再繼續執行。
        使用一個計數器進行實現。計數器初始值爲線程的數量。當每一個線程完成自己任務後,計數器的值就會減一。
        當計數器的值爲0時,表示所有的線程都已經完成一些任務,然後在CountDownLatch上等待的線程就可以恢復執行接下來的任務。*/
        CountDownLatch latch = new CountDownLatch(MAX_TREAD);
        //匿名內部類
        Runnable runnable =  new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count++;
                    atomicInteger.getAndIncrement();
                }
                latch.countDown(); // 當前線程調用此方法,則計數減一
            }
        };
        //同時啓動多個線程
        for (int i = 0; i < MAX_TREAD; i++) {
            new Thread(runnable).start();
        }
        latch.await(); // 阻塞當前線程,直到計數器的值爲0
        System.out.println("理論結果:" + 1000 * MAX_TREAD);
        System.out.println("static count: " + count);
        System.out.println("AtomicInteger: " + atomicInteger.intValue());

理論結果:10000
static count: 9994
AtomicInteger: 10000
每次運行,atomicInteger 的結果值都是正確的,count++的結果卻不對,原因就是AtomicInteger是原子性操作,線程安全的,count不是。

三、Java中的Atomic 原子操作包
JUC 併發包中原子類 , 都存放在 java.util.concurrent.atomic
根據操作的目標數據類型,可以將 JUC 包中的原子類分爲 4 類:
基本原子類、數組原子類、原子引用類型、字段更新原子類
1. 基本原子類
基本原子類的功能,是通過原子方式更新 Java 基礎類型變量的值。基本原子類主要包括了以下三個:
AtomicInteger:整型原子類。
AtomicLong:長整型原子類。
AtomicBoolean :布爾型原子類。
2. 數組原子類
數組原子類的功能,是通過原子方式更新數組裏的某個元素的值。數組原子類主要包括了以下三個:
AtomicIntegerArray:整型數組原子類。
AtomicLongArray:長整型數組原子類。
AtomicReferenceArray :引用類型數組原子類。
3. 引用原子類
引用原子類主要包括了以下三個:
AtomicReference:引用類型原子類。
AtomicMarkableReference :帶有更新標記位的原子引用類型。
AtomicStampedReference :帶有更新版本號的原子引用類型。
AtomicStampedReference 通過引入“版本”的概念,來解決ABA的問題。
4. 字段更新原子類
字段更新原子類主要包括了以下三個:
AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
AtomicLongFieldUpdater:原子更新長整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段。

AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference 這些原子類型,它們無一例外都採用了基於 volatile 關鍵字 +CAS 算法無鎖的操作方式來確保共享數據在多線程操作下的線程安全性。
volatile關鍵字保證了線程間的可見性,當某線程操作了被volatile關鍵字修飾的變量,其他線程可以立即看到該共享變量的變化。
CAS算法,即對比交換算法,是由UNSAFE提供的,實質上是通過操作CPU指令來得到保證的。CAS算法提供了一種快速失敗的方式,當某線程修改已經被改變的數據時會快速失敗。
當CAS算法對共享數據操作失敗時,因爲有自旋算法的加持,我們對共享數據的更新終究會得到計算。

四、 AtomicInteger
1、常用的方法:
public final int get() 獲取當前的值
public final int getAndSet(int newValue) 獲取當前的值,然後設置新的值
public final int getAndIncrement() 獲取當前的值,然後自增
public final int getAndDecrement() 獲取當前的值,然後自減
public final int getAndAdd(int delta) 獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) 通過 CAS 方式設置整數值
AtomicInteger的增減操作都調用了Unsafe實例的方法。private static final Unsafe unsafe = Unsafe.getUnsafe();

五、Unsafe類
Unsafe 是位於 sun.misc 包下的一個類,Unsafe 提供了CAS 方法,直接通過native方式(封裝 C++代碼)調用了底層的 CPU 指令 cmpxchg。
Unsafe 提供的 CAS 方法,主要如下: 定義在 Unsafe 類中的三個 “比較並交換”原子方法
/*
@param o 包含要修改的字段的對象
@param offset 字段在對象內的偏移量
@param expected 期望值(舊的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失敗
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);

六、CAS的缺點
1. ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。
JDK 提供了兩個類 AtomicStampedReference、AtomicMarkableReference 來解決 ABA 問題。
2. 只能保證一個共享變量的原子操作。一個比較簡單的規避方法爲:把多個共享變量合併成一個共享變量來操作。
JDK 提供了 AtomicReference 類來保證引用對象之間的原子性,可以把多個變量放在一個 AtomicReference 實例後再進行 CAS 操作。
比如有兩個共享變量 i=1、j=2,可以將二者合併成一個對象,然後用 CAS 來操作該合併對象的 AtomicReference 引用。
3. 循環時間長開銷大。高併發下N多線程同時去操作一個變量,會造成大量線程CAS失敗,然後處於自旋狀態,導致嚴重浪費CPU資源,降低了併發性。
解決 CAS 惡性空自旋的較爲常見的方案爲:
3.1 分散操作熱點,使用 LongAdder 替代基礎原子類 AtomicLong。
3.2 使用隊列削峯,將發生 CAS 爭用的線程加入一個隊列中排隊,降低 CAS 爭用的激烈程度。JUC 中非常重要的基礎類 AQS(抽象隊列同步器)就是這麼做的。

七、以空間換時間:LongAdder
1. LongAdder 的原理
LongAdder 的基本思路就是分散熱點, 如果有競爭的話,內部維護了多個Cell變量,每個Cell裏面有一個初始值爲0的long型變量, 不同線程會命中到數組的不同Cell(槽 )中,各個線程只對自己Cell(槽)中的那個值進行 CAS 操作。這樣熱點就被分散了,衝突的概率就小很多。
在沒有競爭的情況下,要累加的數通過 CAS 累加到 base 上。
如果要獲得完整的 LongAdder 存儲的值,只要將各個槽中的變量值累加,得到最後的值即可。
LongAdder結構:
函數:sum():long; reset():void; sumThenReset():long; longValue():long; longAccumulate(long,LongBinaryOperator,boolean):void;
成員:Striped64:#base:volatile long; #cellsBusy:volatile int; #cells:volatile Cell[];
/**
* cell表,當非空時,大小是2的冪。
*/
transient volatile Cell[] cells;

/**
* 基礎值,主要在沒有爭用時使用
* 在沒有爭用時使用CAS更新這個值
*/
transient volatile long base;

/**
* 自旋鎖(通過CAS鎖定) 在調整大小和/或創建cell時使用,
* 爲 0 表示 cells 數組沒有處於創建、擴容階段,反之爲1
*/
transient volatile int cellsBusy;
Striped64 內部包含一個 base 和一個 Cell[] 類型的 cells 數組 。 在沒有競爭的情況下,要累加的數通過 CAS 累加到 base 上;如果有競爭的話,會將要累加的數累加到 Cells 數組中的某個 cell 元素裏面。所以 Striped64 的整體值 value 爲 base+ ∑ [0~n]cells 。
Striped64 的設計核心思路就是通過內部的分散計算來避免競爭,以空間換時間。 LongAdder的 base 類似於 AtomicInteger 裏面的 value ,在沒有競爭的情況,cells 數組爲 null ,這時只使用 base 做累加;而一旦發生競爭,cells 數組就上場了。cells 數組第一次初始化長度爲 2 ,以後每次擴容都是變爲原來的兩倍,一直到 cells 數組的長度大於等於當前服務器 CPU 的核數。爲什麼呢?同一時刻,能持有 CPU 時間片而去併發操作同一個內存地址的最大線程數,最多也就是 CPU 的核數。
在存在線程爭用的時候,每個線程被映射到 cells[threadLocalRandomProbe & cells.length] 位置的 Cell 元素,該線程對 value 所做的累加操作,就執行在對應的 Cell 元素的值上,最終相當於將線程綁定到了 cells 中的某個 cell 對象上。

八、使用 AtomicStampedReference 解決 ABA 問題
JDK 的提供了一個類似 AtomicStampedReference 類來解決 ABA 問題。
AtomicStampReference 在 CAS 的基礎上增加了一個 Stamp 整型 印戳(或標記),使用這個印戳可以來覺察數據是否發生變化,給數據帶上了一種實效性的檢驗。
AtomicStampReference 的 compareAndSet 方法首先檢查當前的對象引用值是否等於預期引用,並且當前印戳(Stamp)標誌是否等於預期標誌,如果全部相等,則以原子方式將引用值和印戳(Stamp)標誌的值更新爲給定的更新值。
1、AtomicStampReference 的構造器:
/**
* @param initialRef初始引用
* @param initialStamp初始戳記
*/
AtomicStampedReference(V initialRef, int initialStamp)
2、AtomicStampReference 的常用的幾個方法如下:
public V getRerference() 引用的當前值
public int getStamp() 返回當前的"戳記"
public boolean weakCompareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
expectedReference 引用的舊值;newReference 引用的新值;expectedStamp 舊的戳記;newStamp 新的戳記;
案例:
boolean success = false;
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 0);
int stamp = atomicStampedReference.getStamp();
success = atomicStampedReference.compareAndSet(1, 0, stamp, stamp + 1);
System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp());
//值與戳記都一致,修改成新的值和戳記:success:true;reference:0;stamp:1

stamp = 0;// 修改印戳,更新失敗
success = atomicStampedReference.compareAndSet(0, 1, stamp, stamp + 1);
System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp());
//戳記不同,更新失敗:success:false;reference:0;stamp:1
原文鏈接:https://blog.csdn.net/lwang_IT/article/details/121638089

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