JUC之atomic

Atomic

簡介

atomic包下原子操作類提供了一種用法簡單、性能高效 、線程安全地更新一個變量的方式。

atomic包下一共有12個相關的類,分爲4組,分別用於原子更新基本類型,原子更新數組,原子更新引用,原子更新字段。

使用

  • 原子更新基本類型

    AtomicBoolean AtomicInteger AtomicLong
    常用方法,以AtomicInteger爲例

    • int get() 獲取實際的值
    • void set(int newValue) 設置值,多線程情況下會導致值不安全
    • boolean compareAndSet(int expect, int update) 如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值,並返回是否成功
    • int getAndSet(int newValue) 以原子的方式將值設置爲新值
    • int getAndIncrement() 以原子的方式將值加一,並返回
      以上方法大致可以分爲3類:第一類不保證多線程操作下線程安全,如set();第二類返回boolean類型的值,需要傳入兩個參數,預期值和更新值,如compareAndSet(),這類方法不保證值一定能更新成功,多個線程同時操作,同樣的入參只會有一個線程成功。第三類:getAndIncrement(),這類方法會保證多線程下操作一定成功,它不關心預期的值,只關心更新後的值,假如多個線程同時操作,同一時刻只有一個線程在更新值,其他線程會循環等待之更新成功。
  • 原子更新數組

    AtomicIntegerArray AtomicLongArray AtomicReferenceArray
    常用方法,同樣以AtomicIntegerArray爲例

    • int get(int i) 獲取第i個數組下標中的值
    • void set(int i, int newValue) 設置第i個數組下標中的值
    • boolean compareAndSet(int i, int expect, int update) 輸入的第i個下標等於預期值,則以原子方式將該值設置爲輸入的值,並返回是否成功
    • int getAndSet(int i, int newValue) 以原子的方式將數字第i的值設置爲新值
    • int getAndIncrement(int i) 以原子的方式將第i的值加一,並返回
  • 原子更新字段

    AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater
    常用方法,同樣以AtomicIntegerFieldUpdater爲例

    • AtomicIntegerFieldUpdaterImpl(final Class tclass,final String fieldName,final Class<?> caller) 構造函數需要傳入class 和 字段名稱
    • void set(T obj, int newValue); 類似,不再說明
    • boolean compareAndSet(T obj, int expect, int update) 類似,不再說明
    • int getAndSet(T obj, int newValue) 類似,不再說明
  • 原子更新引用

    AtomicReference AtomicMarkableReference AtomicStampedReference
    常用方法,以AtomicReference爲例

    • void set(V newValue)
    • boolean compareAndSet(V expect, V update)
    • V getAndSet(V newValue)

代碼示例:

多線程下的併發問題

public class SatomicExample1 {

    private static int sum;
    private static AtomicInteger atomicSum = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch c1 = new CountDownLatch(5);
        CountDownLatch c2 = new CountDownLatch(5);


        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                for(int j = 0; j < 100000; j++) {
                    sum++;
                }
                c1.countDown();
            });
        }

        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                for(int j = 0; j < 100000; j++) {
                    atomicSum.getAndIncrement();
                }
                c2.countDown();
            });
        }
        c1.await();
        c2.await();
        System.out.println(sum);
        System.out.println(atomicSum.get());
        executorService.shutdown();

    }
}

輸出結果

494796
500000

使用atomic實現簡單的自旋鎖

public class SatomicExample2 {

    private static int sum;

    public static void main(String[] args) throws InterruptedException {
        TestLock lock = new TestLock();
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CountDownLatch c1 = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                for(int j = 0; j < 100000; j++) {
                    try {
                        lock.lock();
                        sum++;
                    } finally {
                        lock.unLock();
                    }
                }
                c1.countDown();
            });
        }
        c1.await();
        System.out.println(sum);
        executorService.shutdown();
    }
}
class TestLock {

    private AtomicBoolean locked = new AtomicBoolean();

    public void lock() {
        while (!locked.compareAndSet(false, true)) {}
    }

    public void unLock() {
        locked.set(false);
    }

}

結果

500000

lazySet

我們可以觀察到,大多數Atomic類中都有lazySet()這個方法,這個方法有什麼用呢?以AtomicInteger爲例,通過觀察源碼可以發現,變量value是通過volatile修飾的,volatile在保證value可見性的同時也會比不加volatile修飾的更浪費cpu性能。我們知道volatile是通過設置內存屏障來實現的,對於某些確定不需要加內存屏障的情況下,volatile勢必會浪費性能。所以Doug Lea大神提供也lazySet()這個方法提供了一個可優化的選項。當然,如果使用錯誤,會導致一些很嚴重的問題。

ABA 問題

假如一個值原來是A,變成了B,又變成了A,那麼CAS檢查時會發現它的值沒有發生變化,但是實際上卻變化了。

通過引入 AtomicStampedReference AtomicMarkableReference來解決問題

AtomicStampedReference 提供了boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) ,在原有需要提供預期值,更新值之外,還需要提供預期版本號和更新版本號,假如預期值符合預期,允許更新,但是版本號不符合預期,同樣不會更新成功。

AtomicStampedReferenceAtomicMarkableReference的區別是
AtomicStampedReference關注最新的版本號,而AtomicMarkableReference只關注最新是否更改過,換個說法假如一個更新的過程是這樣的A->B->A->B->A,AtomicStampedReference關注最後的A到底是哪個版本號的A,而AtomicMarkableReference只關注A是否產生了ABA問題

原理

我們以AtomicInteger爲例

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

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看出都是調用了unsafe的方法
Unsafe源碼

    /**
         * Atomically update Java variable to <tt>x</tt> if it is currently
         * holding <tt>expected</tt>.
         * @return <tt>true</tt> if successful
         */
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);


    /**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object <code>o</code>
     * at the given <code>offset</code>.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

可以看出getAndAddInt()就是通過自旋compareAndSwapInt()來實現的,接着看compareAndSwapInt()

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

最終又是通過 Atomic::cmpxchg來實現的

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

最後我們終於看到了最原始的彙編指令cmpxchg

cpmxchg

cmpxchg是彙編指令
作用:比較並交換操作數.
如:CMPXCHG r/m,r 將累加器AL/AX/EAX/RAX中的值與首操作數(目的操作數)比較,如果相等,第2操作數(源操作數)的值裝載到首操作數,zf置1。如果不等, 首操作數的值裝載到AL/AX/EAX/RAX並將zf清0
該指令只能用於486及其後繼機型。第2操作數(源操作數)只能用8位、16位或32位寄存器。第1操作數(目地操作數)則可用寄存器或任一種存儲器尋址方式。

相關資料

openjdk 源碼下載

Doug Lea的cookbook

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