原子變量和CAS


爲什麼需要它們?

public class Counter{
 private int count;
 public synchronized void incr(){
    count ++;
 }
 
 public synchronzied int getCount(){
   return count;
 }
}

對於count++ 這種操作來說,使用synchronized 成本太高了,需要先獲取鎖,最後需要釋放鎖,獲取不到鎖的情況下需要等待,還會有線程的上下文切換,這些都是需要成本的。

對於以上這種情況,完全可以使用原子變量代替。

原子變量類型

AtomicBoolean : 原子Boolean 類型,常用來程序中表示一個標誌位。

AtomicInteger: 原子Integer 類型。

1.基本用法

// 構造方法
public AtomicInteger(int initialValue); // 給定一個初始值

public AtomicInteger(); // 初始值爲0

// set 和 get 方法
public final int get();

public final void set(int newValue);

2.原子方式實現組合操作的方法

// 以原子方式獲取舊值並設置新值
    public final int getAndSet(int newValue)// 以原子方式獲取舊值並給當前值加1
    public final int  getAndIncrement();
    
    // 以原子方式獲取舊值並給當前值減1 
     public final int getAndDecrement();
     
    // 以原子方式獲取舊值並給當前值加delta
    public  final int getAndAdd(int delta);
    
    // 以原子方式給當前值加1並獲取新值
    public final int incrementAndGet();
    
    //以原子方式給當前值減1並獲取新值
    public final int decrementAndGet();
    
    // 以原子方式給當前值加delta並獲取新值
    public final int addAndGet(int delta);

這些方法都依賴另一個public 方法:

public final boolean compareAndSet(int expect, int update)

compareAndSet 方法,比較並設置,我們簡稱爲CAS。 該方法有兩個參數expect 和 update,以原子方式實現瞭如下功能:如果當前值等於expect ,則更新爲update,否則不更新,如果更新成功,返回true,否則返回false。

3.AtomicInteger 可以在程序中用作一個計數器,多個線程併發更新,也總能實現正確性。

package com.claa.javabasic.Thread;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: claa
 * @Date: 2020/04/18 22:33
 * @Description: AtomicInteger 實例
 */
public class AtomicIntegerDemo {
     private static AtomicInteger counter = new AtomicInteger(0);

     static  class Visitor extends Thread {
         @Override
         public void run() {
             for(int i = 0; i < 1000; i++){
                 counter.incrementAndGet();
             }
         }
     }

    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        
        Thread[] threads = new Thread[num];
        
        for(int i =0; i < num; i++){
            threads[i] = new Visitor();
            threads[i].start();
        }
        
        for(int i = 0; i < num; i++ ){
            threads[i].join();
        }

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


}

4.基本實現方式

// 主要內部成員,聲明帶有volatile ,這是必須的,以保證內存的可見性
private volatile int value;

// incrementAndGet方法具體代碼
public final int incrementAndGet(){
    for(;;){
        int current = get();
        int next = current +1;
        if(compareAndSet(current,next))
            return next;
    }
}

代碼主體是一個死循環,先獲取當前值current,計算期望的值next ,然後調用CAS方法進行更新,如果更新沒有成功,說明value 被別的線程改了,則再去取最新值並嘗試更新直到成功爲止。

AtomicLong: 原子Long類型,常用來在程序中生成唯一序列號。

AtomicReference: 原子引用類型,用來以原子方式更新複雜類型。

與synchronized 比較

synchronized 是悲觀的,它假定更新很可能衝突,所以先獲取鎖,得到鎖後才更新;

原子變量的更新邏輯是樂觀的,它假定衝突比較少,但使用CAS更新,也就是進行衝突檢測,如果確實衝突了,那也沒有關係,繼續嘗試就好了。

synchronized 代表一種阻塞式算法,得不到鎖的時候,進入鎖等待隊列,等待其他線程喚醒,有上下文切換開銷。

原子變量的更新是非阻塞式的,更新衝突的時候,它就重試,不會阻塞,不會有上下文切換開銷。對於大部分比較簡單的操作,無論是在低併發還是高併發情況下,這種樂觀非阻塞方式的性能都遠高於悲觀阻塞方式。

compareAndSet是如何實現的呢?

public final boolean compareAndSet(int expect, int update){
    return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}

unsafe 是什麼?它的類型爲sun.misc.Unsafe

public static final Unsafe unsafe = Unsafe.getUnsafe();

它是Sun的私有實現,從名字看,表示的也是“不安全”,一般應用程序不應該直接使用,原理上,一般的計算機都在硬件層次上支持CAS 指令,而Java的實現都利用這些特殊的指令。

基於CAS 實現鎖

package com.claa.javabasic.Thread;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: claa
 * @Date: 2020/04/19 08:26
 * @Description: 基於CAS  實現的鎖
 */
public class MyLock {
    private AtomicInteger status = new AtomicInteger(0);
    
    public  void lock(){
        while( ! status.compareAndSet(0,1)){
            Thread.yield();
        }
    }
    
    public  void unlock(){
        status.compareAndSet(1,0);
    }
}


在 MyLock 中,使用status 表示鎖的狀態,0表示未鎖定,1表示鎖定,lock()、unlock() 使用CAS 方法更新,lock()只有在更新成功後才退出,實現了阻塞的效果,不過這種阻塞方式過於消耗CPU,實際開發中使用ReentrantLock

ABA 問題

該問題是指 ,假設當前值爲A,如果另一個線程先將A修改爲B,再修改回成A,當前線程的CAS 操作無法辨認當前值發生過變化。

解決方法是使用AtomicStampedReference,在修改值得同時附加一個時間戳,只有值和時間戳都相同才進行修改,其CAS方法聲明爲:

public boolean compareAndSet(
   V expectedReference ,V newReference, int expectedStamp, int newStamp
) 
Pair pair = new Pair(100,200);

int stamp = 1;

AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<Pair>(pair,stamp);

int newStamp =2;

pairRef.compareAndSet(pair,new Pair(200,200),stamp,newStamp);


AtomicStampedReference 在 compareAndSet中要同時修改兩值:一個是引用,一個是時間戳。它怎麼實現原子性呢? 實際上,內部AtomicStampedReference會將兩個值組合成爲一個對象,修改的是一個值。

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)) );
}
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);
    }
}

AtomicStampedReference 將引用值和時間戳的組合比較和修改轉換爲了對這個內部類Pair 單個值的比較和修改。

參考文章

java編程的邏輯基礎(馬俊昌)

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