併發編程--CAS深入理解,以及ABA問題的處理!

       hello大家好,好久不見我是小卡。好久沒用更新博客了,今天有點時間,就來個大家談談我們在代碼中經常使用到的一些容器的底層的相關算法,寫得不對的希望大家評論留言、一鍵三連。

 

       在日常的高併發、多線程的開發中,通常使用的hashMap就無法滿足我們的需求了,因爲hashMap中的所有操作的是沒有加鎖的,所以在高併發的情況下可能會出現數據安全性問題

 

       有朋友會問,爲啥不用hashTable呢?hashTable不是線程安全的嗎?

       有這樣想法朋友你還在第一層,其實呢我已經在第五層了,起飛!!!爲什麼我會這樣講,首先hashTable的確是線程安全的,我們來看一看他的幾個方法:

    public synchronized V put(K key, V value){......}     
    public synchronized V get(Object key){......}

        hashTable裏面方法都是見了synchronized關鍵字的,synchronized關鍵字是一個獨佔鎖(是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖)非常的笨重,消耗大量的cpu資源。基本是不會使用滴。所以小卡說你是第一層。

          

        我們實際開發中高併發使用的安全容器叫做concurrentHashMap。concurrentHashMap是一個非常牛逼、也是一個使用非常普遍的線程安全容器。首先我們來看看他究竟是個啥:

    public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable

         我們再來看看他的核心方法: 

    public V put(K key, V value) {
       return putVal(key, value, false);
    }  
    final V putVal(K key, V value, boolean onlyIfAbsent) {
       if (key == null || value == null) throw new NullPointerException();
       int hash = spread(key.hashCode());
       int binCount = 0;
       for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
                }   ......

        其中最重要的就是這個casTabAt(),首先各位鐵汁,如果你看到casTabAt()這裏,恭喜你鐵汁,你已經到了第三層了。 我們一步一步往下看,看看和這個casTabAt()究竟是何方神聖:

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
     }

        這就到咱們今天要說的關鍵:CAS算法(compareAndSwap),這裏點進去搞清楚,那麼恭喜你,起飛了!!!

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

        鐵汁們看見和這個native了嗎?這底層是c語言寫的,在idea裏面這裏就點不進去了!那怎麼辦呢?

        Oracle的jdk的源碼就到sun.misc.Usafe.java這裏就結束了,native方法不能忘下面再走了。這時候我使用百度搜索,發現openJdk裏面的這段代碼可以查看。

        於是我這邊就去gitHub上面下載openJdk的源碼,希望通過這些代碼能夠徹底理解cas方法的核心

        去github下載是真的慢,所以弟弟我在這裏給各位有興趣看的規格準備好了免費的提取鏈接

        openJdk源碼下載:鏈接: https://pan.baidu.com/s/1E9MHL8JcYSlbSImU1_U2WQ 提取碼: xpmt

        首先點開Usafe.java方法:

    public final class 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 compareAndSwapObject(Object o, long offset,Object expected,Object x);
          ...... 
    }

         能理解到這一層的鐵汁萌,已經是第四層了,爲什麼說不是第五層呢?因爲這還不是最底層的,那麼就我們一起進入第五層,起飛!

         先來張圖片: 

usafe.cpp.png

         

          我尼瑪炸了呀!.cpp這是什麼文件啊?我不到啊?馬上百度一下:

         

        原來是c++文件,.cpp =  c plus plus,漲知識了!但是俺還是看不懂,在已我java的邏輯查看一遍之後又去請教c++組的同事將這段cas的邏輯理解。    

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
  UnsafeWrapper("Unsafe_CompareAndSwapObject");
  // 新值
  oop x = JNIHandles::resolve(x_h);
  // 預期值
  oop e = JNIHandles::resolve(e_h);
  // 內存值
  oop p = JNIHandles::resolve(obj);
  // 計算p在堆內存中的具體位置
  HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);
  // 調用另一個原子性操作方法,並返回結果
  oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);
  // 如果返回的res等於e,則判定滿足compare條件(說明res應該爲內存中的當前值),但實際上會有ABA的問題
  jboolean success  = (res == e);
  if (success)
  // success爲true時,說明此時已經內存值和預期值一致,這時候將x的值放在p的堆內存位置
  // 調用的是最底層的cmpxchg指令(計算機指令!!!)
    update_barrier_set((void*)addr, x);
  return success;
UNSAFE_END

        不難發現,這裏的最關鍵的方法還是 atomic_compare_exchange_oop(x, addr, e, true),因爲最重要的步驟就是這一步,只有在將預期值e 和 內存值p 比較得到相同的結果之後,才能夠進行下一步的替換操作,那麼我們順藤摸瓜,繼續往下走。

         全局搜索atomic_compare_exchange_oop方法,找到的第一個是oop.hpp的類:

    static oop atomic_compare_exchange_oop(oop exchange_value,
                                       volatile HeapWord *dest,
                                       oop compare_value,
                                       bool prebarrier = false);

         // 然後在點開他的實現類

inline oop oopDesc::atomic_compare_exchange_oop(oop exchange_value,
                                                volatile HeapWord *dest,
                                                oop compare_value,
                                                bool prebarrier) {
 // 如果使用了壓縮普通對象指針(CompressedOops),有一個重新編解碼的過程 
 if (UseCompressedOops) { 
    if (prebarrier) {
      update_barrier_set_pre((narrowOop*)dest, exchange_value);
    }
    // encode exchange and compare value from oop to T
    narrowOop val = encode_heap_oop(exchange_value); // 新值
    narrowOop cmp = encode_heap_oop(compare_value); // 預期值
    narrowOop old = (narrowOop) Atomic::cmpxchg(val, (narrowOop*)dest, cmp); 
    // decode old from T to oop
    return decode_heap_oop(old);
  } else {
    if (prebarrier) {
      update_barrier_set_pre((oop*)dest, exchange_value);
    }
    // 可以看到這裏繼續調用了其他方法
    return (oop)Atomic::cmpxchg_ptr(exchange_value, (oop*)dest, compare_value); 
  }
}

現在我們來看最後一層:

inline jlong    Atomic::cmpxchg    (jlong    exchange_value, volatile jlong*    dest, jlong    compare_value) {
  int mp = os::is_MP();
  jint ex_lo  = (jint)exchange_value;
  jint ex_hi  = *( ((jint*)&exchange_value) + 1 );
  jint cmp_lo = (jint)compare_value;
  jint cmp_hi = *( ((jint*)&compare_value) + 1 );
  __asm {
    push ebx
    push edi
    mov eax, cmp_lo
    mov edx, cmp_hi
    mov edi, dest
    mov ebx, ex_lo
    mov ecx, ex_hi
    LOCK_IF_MP(mp)
    cmpxchg8b qword ptr [edi]
    pop edi
    pop ebx
  }
}

        說實話看到這裏,小卡我看不懂了,這個cmpxchg方法裏面的方法好像都是些計算機指令了,大概的意思就是如何比較兩個node的值吧。有興趣的鐵汁可以執行研究一哈。

 

        再來說一下剛剛提到的ABA的問題。什麼是ABA,爲什麼會出現ABA?

        ABA問題指的是,在使用CAS算法多預期值和內存值作比較的時候存在一種情況,預期值爲A,內存值也爲A,但是內存值的這個A實在被其他線程操作過後的A(old : A, Thread1 -> B , Thread2 -> A),這時候雖然在做 atomic_compare_exchange_oop 比較時的結果是success,但是確沒內味兒了。

        爲啥會出現這種情況?在多cpu的服務器中可能會出現多線程操作這個容器,並同時執行CAS,因爲哥哥cpu之前的任務調度排序不同,執行的速度也可能會不同,就可能會出現A還在執行compare方法的時候,B線程已經執行完swap操作,同時將內存值修改成了A線程的預期值,這時候計算機以爲操作是對的。但是這確實有個錯誤的操作。

 

        如何避免ABA這種情況發生?    

       Java中提供了AtomicStampedReference和AtomicMarkableReference來解決ABA問題 。

        AtomicStampedReference可以原子更新兩個值:引用和版本號,通過版本號來區別節點的循環使用。

 

        這就是目前小卡對cas已經ABA的理解,希望大家支持,下一期我們就來看看AtomicStampedReference和AtomicMarkableReference這兩個東東。

        鐵汁們,一鍵三連!!!

        

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