1. 程式人生 > >原子操作類(一)原子操作類詳細介紹

原子操作類(一)原子操作類詳細介紹

expected 文章 span del 也有 pair 上一個 ride 操作類

引言

??Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環境下,無鎖的進行原子操作。原子變量的底層使用了處理器提供的原子指令,但是不同的CPU架構可能提供的原子指令不一樣,也有可能需要某種形式的內部鎖,所以該方法不能絕對保證線程不被阻塞。

??因為變量的類型有很多種,所以在Atomic包裏一共提供了13個類,可分為4種類型的原子更新方式,分別是

  • 原子更新基本類型
  • 原子更新數組
  • 原子更新引用
  • 原子更新屬性(字段)

Atomic包裏的類基本都是使用Unsafe實現的包裝類。

一、基本類型的原子操作類

Atomic 包提供了以下3個基本類型的原子操作類:

  • AtomicBoolean: 原子更新布爾類型;
  • AtomicInteger: 原子更新整形;
  • AtomicLong: 原子更新長整形;

以上3個類提供的方法幾乎一模一樣,所以本文僅以AtomicInteger為例,常用方法如下:

  • int addAndGet(int delta): 以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果
  • boolean compareAndSet(int expect,int update): 如果輸入的數值等於預期值,則以原子方式將該值設置為輸入的值
  • int getAndIncrement(): 以原子方式將當前值加1,註意,這裏返回的是自增前的值,相當於 (i++)
    的形式。
  • void lazySet(int newValue): JDK6所增加,最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。關於該方法的更多信息可以參考並發編程網翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/howdoes-atomiclong-lazyset-work/”
  • int getAndSet(int newValue): 以原子方式設置為newValue的值,並返回舊值。

@ Example1 ?AtomicInteger 使用例子

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest { static AtomicInteger ai = new AtomicInteger(1); public static void main(String[] args) { System.out.println(ai.getAndIncrement()); System.out.println(ai.get()); } }

運行結果:

1
2

其余的基本類的原子操作類的解決方案

??java 的基本類型一共有8個,然而JDK卻只提供了三個,如何原子的更新其他的基本類型呢?特別是常用的charfloatdouble。Atomic包裏的類基本都是使用Unsafe實現的,讓我們一起看一下Unsafe的源碼.

 /**
     * 如果當前數值是expected,則原子的將Java變量更新成x
     * @return 如果更新成功則返回true
     */
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
    
    public final native boolean compareAndSwapInt(Object o, long offset, int expected,int x);

    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

??通過源碼,可以發現Unsafe只提供了3種CAS方法,那麽 AtomicBoolean 是如何實現的呢,我們再來看一下 AtomicBoolean 的源碼。只給出關鍵部分

public class AtomicBoolean implements java.io.Serializable {
//存儲值:1,0;1代表 true,0 代表 false 
private volatile int value;
//Unsafe 對象
 private static final Unsafe unsafe = Unsafe.getUnsafe();


public final boolean compareAndSet(boolean expect, boolean update) {
        //boolean 類型 轉換成 整形,true 對應 1,false 對應 0;
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }

public final void set(boolean newValue) {
        value = newValue ? 1 : 0
    }

public final boolean get() {
        return value != 0;
    }

//.......
}

??從AtomicBoolean 的源碼可以看出,它是先把 boolean 的值轉換成整型來存儲,再使用compareAndSwapInt進行CAS。所以其他基本類型的原子類型也可參照這個思路來實現。

??思路有了,接下來考慮怎麽實現。bytecharint 的轉換很容易,他們的原子操作類就很容易實現了。那floatdouble 要怎樣才能以整形或者長整形來存儲值呢?
??Float 類中 提供了static兩個方法:通過 Static int floatToIntBits(float value) (根據 IEEE 754 浮點“單一格式”位布局,返回指定浮點值的表示形式)方法,便可以將float 的值以整形的方式存儲下來。如果要取值,則通過 static float intBitsToFloat(int bits)方法,將存儲下來的整形的浮點值轉換回原來的float值。
??同理,Double 類也提供了類似的兩個方法:static long doubleToLongBits(double value) 產生long類型的浮點值。static double longBitsToDouble(long bits)long的浮點值轉換回double值。

下面是 AtomicDouble 的實現例子:

@ Example2 ?AtomicDouble 的實現例子

import java.util.concurrent.atomic.AtomicLong;

public class AtomicDouble extends Number {

    private AtomicLong bits;
    
    public AtomicDouble() {
        this(0d);
    }
    
    public AtomicDouble(Double value){
        bits = new AtomicLong(Double.doubleToLongBits(value));
    }

    public final double get() {
        return Double.longBitsToDouble(bits.get());
    }

    public final void set(double value) {
        bits.set(Double.doubleToLongBits(value));
    }

    public final double addAndGet(double delta) {
        long newBits = bits.addAndGet(Double.doubleToLongBits(delta));
        return Double.longBitsToDouble(newBits);
    }
    
    public final double getAndAdd(double delta) {
        long oldBits = bits.getAndAdd(Double.doubleToLongBits(delta));
        return Double.longBitsToDouble(oldBits);
    }

    public final double getAndSet(double newValue) {
        long oldBits = bits.getAndSet(Double.doubleToLongBits(newValue));
        return Double.longBitsToDouble(oldBits);
    }

    public final boolean compareAndSet(double expect, double update) {
        return bits.compareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    }

    public final boolean weakCompareAndSet(double expect, double update) {
        return bits.weakCompareAndSet(Double.doubleToLongBits(expect), Double.doubleToLongBits(update));
    }
    
    public final void lazySet(double newValue) {
         bits.lazySet(Double.doubleToLongBits(newValue));
    }

    @Override
    public int intValue() { return (int) this.get(); }
    @Override
    public long longValue() { return (long) this.get(); }
    @Override
    public float floatValue() { return (float) this.get();}
    @Override
    public double doubleValue() { return this.get(); }
}

關於原子操作類實現其余猜想:

??除了上面的那種實現方案外,在 stackOverflow 上也發現兩個原子操作類的實現方案:

  1. 利用 AtomicReference 類;如 Float 的原子操作類可以是 AtomicReference<Float>。不可,經過測試,這種方法是不行的,因為像compareAndSet這類方法比較的是對象的內存地址,而不會使用equal()方法進行比較。
  2. 使用JDK1.8新增的方法:DoubleAdderDoubleAccumulator。沒去深入了解,粗略看了下,這兩個類都是適合於“多寫少讀”的情況。

二、數組的原子操作類

通過原子的方式更新數組裏的某個元素,Atomic包提供了以下3個類:

  • AtomicIntegerArray: 原子更新整型數組裏的元素
  • AtomicLongArray: 原子更新長整型數組裏的元素
  • AtomicReferenceArray: 原子更新引用類型數組裏的元素

這幾個類的方法幾乎一樣,以AtomicIntegerArray為例,AtomicIntegerArray類主要是提供原子的方式更新數組裏的整型;

構造方法:
AtomicIntegerArray(int length): 創建給定長度的新 AtomicIntegerArray。
AtomicIntegerArray(int[] array): 創建與給定數組具有相同長度的新 AtomicIntegerArray,並從給定數組復制其所有元素。

其常用方法:
int addAndGet(int i, int delta): 以原子方式將輸入值與數組中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update): 如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

@ Example3 ?AtomicIntegerArray 的使用例子

public class AtomicIntegerArrayTest {

    static int[] value = new int[] { 1, 2 };

    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
                System.out.println(value[0]);
    }
}

運行結果:

3
1

AtomicIntegerArray類需要註意的是 ,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組復制一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響到傳入的數組。


三、引用類型的原子操作類

?基本類型的原子操作類只能更新基本類型的值,不能更新引用類型的對象引用。Atomic包提供了以下三個類,可以原子方式更新的對象引用。

  • AtomicReference: 原子更新引用類型。
  • AtomicStampedReference: 原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更數據和數據的版本號,可以解決使用CAS進行原子更新時,可能出現的ABA問題。
  • AtomicMarkableReference: 原子更新帶有標記位的引用類型。可以原子的更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

1. AtomicReference 介紹

構造方法

AtomicReference(): 使用 null 初始值創建新的 AtomicReference
AtomicReference(V initialValue): 使用給定的初始值創建新的 AtomicReference

常用的方法: 與前面介紹的類差不多,get()set(V newValue)compareAndSet(V expect, V update)getAndSet(V newValue);

@ Example4 ?AtomicReference 的使用例子

public class AtomicReferenceTest {

    public static AtomicReference<user> atomicUserRef = new AtomicReference</user><user>();

    public static void main(String[] args) {
        User user = new User("conan", 15);
        atomicUserRef.set(user);
        User updateUser = new User("Shinichi", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        System.out.println(atomicUserRef.get().getName());
        System.out.println(atomicUserRef.get().getOld());
    }

    static class User {
        private String name;
        private int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

運行結果:

Shinichi
17

2. AtomicMarkableReference 與 AtomicStampedReference 介紹

??這兩個類的作用與 AtomicReference 的作用非常相似,都是原子更新引用類型。但他們還有一個作用:能解決CAS過程中的ABA問題

什麽是ABA問題?

??ABA的問題就是:在多線程的環境下,線程1將A的值賦給了B,然後B的值又重新賦值了A。在這個過程中,A已經被修改了一次了,但是線程2不知道,在進行CAS時,認為A的值沒有被修改過,所以就進行修改。當然,如果只對結果敏感,而對修改的次數不敏感,那麽這個問題就無所謂了。

AtomicStampedReference 和 AtomicMarkableReference 是怎麽解決ABA問題的?

??這兩個類的解決方案也很簡答,就是多維護了一個標誌位記錄修改的狀態 或者 維護一個版本號記錄修改的次數,然後進行CAS時,也會比較標誌位或者版本號。簡單看一下源碼吧。

AtomicMarkableReference 用的是標誌位mark(布爾類型)來標誌修改的狀態,下面是關鍵部分源碼:

//構造方法,initialRef 是 初始的引用類型,initialMark 是初始的標誌位
public AtomicMarkableReference(V initialRef,  boolean initialMark);

//CAS 不僅要比較引用類型,還要比較標誌位
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedMark == current.mark &&
            ((newReference == current.reference &&
              newMark == current.mark) ||
             casPair(current, Pair.of(newReference, newMark)));
 }

AtomicStampedReference 用的則是版本號stamp(整形int) 來記錄修改的次數,下面是關鍵部分的源代碼:

//構造方法,初始引用類型 和 初始版本號
 public AtomicStampedReference(V initialRef, int initialStamp);

//CAS 不僅要比較引用類型,還要比較版本號
  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)));
    }

@ Example5 ?AtomicStampedReference 的解決ABA問題的例子

看一個AtomicStampedReference的例子。當需要執行一次更新當前用戶的操作,這裏故意將更新的用戶就是當前用戶,也就是更新後對象引用是沒有變化的。線程A、線程B 都接到這個任務,但只能執行一次,意味著得有一個線程更新失敗。User類的代碼請查看上一個例子。

public class AtomicStampedReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        User user = new User("小赫",25);
        //初始版本為1
        int stamp = 1;
        
        //設置當前用戶是  小赫
        AtomicStampedReference<User> currentAtomicUser = new AtomicStampedReference<User>(user, stamp);
        
        //更新當前用戶,更新的目標用戶就是當前用戶
        UpdateUserTask task = new UpdateUserTask(stamp, currentAtomicUser, user, user);
        //線程A,線程B執行相同的更新任務,但更新操作只需要執行一次。
        Thread threadA = new Thread(task,"ThreadA");
        Thread threadB = new Thread(task,"ThreadB");
        threadA.start();
        threadB.start();
    }
}

class UpdateUserTask implements Runnable{

    private int stamp;
    private AtomicStampedReference<User> currentAtomicUser;
    private User newUser;
    private User oldUser;
    
    public UpdateUserTask(int stamp, AtomicStampedReference<User> currentAtomicUser,User newUser,User oldUser) {
        this.stamp = stamp;
        this.currentAtomicUser = currentAtomicUser;
        this.newUser = newUser;
        this.oldUser = oldUser;
    }

    @Override
    public void run() {
        //更新當前用戶,版本號加一
        boolean b = currentAtomicUser.compareAndSet(oldUser, newUser, stamp, stamp+1);
        if(b){//更新執行成功
            System.out.println("線程"+Thread.currentThread().getName()+": 成功執行更新操作");
        }else{//更新失敗
            System.out.println("線程"+Thread.currentThread().getName()+": 當前用戶是 "+ currentAtomicUser.getReference().getName()+" ,版本號已經過期,更新操作已經被其他線程完成");
        }
    }
     
 }

運行結果:

線程ThreadB: 當前用戶是 小赫 ,版本號已經過期,更新操作已經被其他線程完成
線程ThreadA: 成功執行更新操作

??從結果可以看出,當線程B執行任務時,盡管當前用戶的對象引用沒有改變,但版本號卻已經改變了,線程B從而知道了更新操作已經被執行了,於是便不再執行更新。


四、 更新字段的原子操作類

如果我們只需要某個類裏的某個字段,那麽就需要使用原子更新字段類,Atomic包提供了以下三個類:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。通過靜態工廠方法創建newUpdater(Class<U> tclass, String fieldName)
  • AtomicLongFieldUpdater: 原子更新長整型字段的更新器。通過靜態工廠方法創建newUpdater(Class<U> tclass, String fieldName)
  • AtomicReferenceFieldUpdater 原子更新引用類型裏的字段。比前兩個方法的範圍要廣,可以更新任意類型的字段,通過靜態的工廠方法創建 newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

原子更新字段類都是抽象類,每次使用都時候必須使用靜態方法newUpdater創建一個更新器。原子更新類的字段的必須使用public volatile修飾符。以上3個類提供的方法差不多,下面給出AtomicIntegerFieldUpdater 的例子:

@ Example6 ?AtomicIntegerFieldUpdater 例子

public class AtomicIntegerFieldUpdaterTest {

    private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater
            .newUpdater(User.class, "old");

    public static void main(String[] args) {
        User conan = new User("conan", 10);
        System.out.println(a.getAndIncrement(conan));
        System.out.println(a.get(conan));
    }

    public static class User {
        private String name;
        public volatile int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public int getOld() {
            return old;
        }
    }
}

運行結果:

10
11

原子操作類(一)原子操作類詳細介紹