JAVA多線程並行計算樂觀鎖之Atomic系列詳解

  1. 從多線程並行計算樂觀鎖 和 悲觀鎖 來講,JAVA中的 lock、synchronized 屬於悲觀鎖,即是在操作某數據的時候總是會認爲多線程之間會相互干擾,屬於阻塞式的加鎖;Atomic系列則屬於樂觀鎖系列,即當操作某一段數據的時候,線程之間是不會相互影響,採用非阻塞的模式,直到更新數據的時候纔會進行版本的判斷是否值已經進行了修改。

  2. Atomic在JAVA中的家族如下:
    a、基本類:AtomicInteger、AtomicLong、AtomicBoolean;
    b、引用類型:AtomicReference、AtomicReference的ABA實例、AtomicStampedRerence、AtomicMarkableReference;
    c、數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
    d、屬性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

  3. 基本類型以AtomicInteger爲代表進行講解。
    首先我們來看看AtomicInteger的關鍵性代碼如下:

    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }

通過以上代碼可以發現,內部定義了一個 volatile 修飾的 value,volatile修飾的變量我們知道保證了內存的的可見性,也就是volatile定義的值只會存在於內存,而拋棄了多核或者多CPU的多級緩存。

volatile保證了內存可見性,而sun.misc.Unsafe 則保證了原子性操作,因爲Unsafe中引用了大量的native定義的本地代碼,也就是C代碼,並且採用CAS(compare and swap) 模式,保證了每次值的更新只會有一個線程會更新成功,其他更新失敗的線程會進行循環,直到成功爲止。

通過以上2點,這裏找到了爲什麼Atomic除了能保證 volatile所具備的內存可見性功能外,還具備了原子性操作的原理。

請見unsafe的調用方法:

 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);//本方法採用了關鍵性的CAS操作。

除了AtomicStampedRerence 和 AtomicMarkableReference,其他類都會存在ABA問題,ABA問題請詳見上一篇博文的樂觀鎖 和 悲觀鎖的解釋,AtomicStampedRerence 和 AtomicMarkableReference 分別通過版本號 和 標記來解決ABA問題,以下以AtomicStampedRerence 爲例。

AtomicStampedRerence 源碼解析:

    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;


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

通過以上源碼可以得到:
a、AtomicStampedRerence 是通過對象引用 reference 和 版本號 stamp共同來控制CAS操作。
b、在compareAndSet 可以發現,只有新值 和 期望值都符合要求才算成功。

  • ABA測試:
 public static void main(String []args) {
        for(int i = 0 ; i < 100 ; i++) {
            final int num = i;
            new Thread() {
                public void run() {
                    try {

                        Thread.sleep(Math.abs((int)(Math.random() * 100)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2")) {
                        System.out.println("我是線程:" + num + ",我獲得了鎖進行了對象修改!");


                    }
                }
            }.start();
        }
        new Thread() {
            public void run() {
                System.out.println("最初值:" +  ATOMIC_REFERENCE.get());
                while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc"));
                System.out.println("結束值:" +  ATOMIC_REFERENCE.get());
                System.out.println("已經改爲原始值!");
            }
        }.start();
    }

a、根據Atomic的原理可知,for(int i = 0 ; i < 100 ; i++) 中雖然創建了 100個線程,但是有且只有一個線程對 ATOMIC_REFERENCE.compareAndSet(“abc” , “abc2”) 進行修改,即是 abc 被修改成 abc2只有一個線程能修改成功,而當值被修改成 abc2之後,其他線程再次進來發現堆棧中不是 abc而是 abc2 則 總是失敗。
b、另外開一個線程將 abc2修改爲 abc之後,for循環中的後續線程當發現堆棧中是abc,那麼就回將其重新修改爲abc2,如下圖運行結果所示。
這裏寫圖片描述

AtomicStampedRerence 解決 ABA問題:

 public static void main(String []args) {
        for(int i = 0 ; i < 100 ; i++) {
            final int num = i;
            int stamped = ATOMIC_REFERENCE.getStamp();
            new Thread() {
                public void run() {
                    try {

                        Thread.sleep(Math.abs((int)(Math.random() * 100)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2", stamped, stamped + 1)) {
                        System.out.println("我是線程:" + num + ",我獲得了鎖進行了對象修改!");


                    }
                }
            }.start();
        }
        int stamped1 = ATOMIC_REFERENCE.getStamp();
        new Thread() {
            public void run() {
                System.out.println("最初值:" +  ATOMIC_REFERENCE.getReference());
                while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc", stamped1, stamped1 + 1));
                System.out.println("結束值:" +  ATOMIC_REFERENCE.getReference());
                System.out.println("已經改爲原始值!");
            }
        }.start();
    }

直接貼出以上採用 AtomicStampedRerence 的運行結果:
這裏寫圖片描述

從運行結果可以發現,解決掉了ABA問題,所有線程中,有且只有一個線程修改了值。

參見:https://blog.csdn.net/xh16319/article/details/17056767

發佈了66 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章