1. 程式人生 > >執行緒併發--CocurrentHashMap和CopyOnWriteArrayList詳解

執行緒併發--CocurrentHashMap和CopyOnWriteArrayList詳解

在多執行緒開發中,我們經常要考慮執行緒併發的問題,那麼如何來避免執行緒併發程式碼的資料讀寫問題呢?

我們常見的HashMap、TreeMap、LinkedList、ArrayList都是執行緒不安全的,而Java也提供了一些執行緒安全的容器類:

如:

各種併發容器:CocurrentHashMap、CopyOnWriteArrayLis等;

各種執行緒安全佇列(Queue/Deque):ArrayBlockingQueque、SynchronousQueue等;

各種有序容器的執行緒安全版本等。

下面我們就來說一說CocurrentHashMap和CopyOnWriteArrayList是如何實現高效執行緒安全的?

記得當初在剛學習java時,遇到可能存在併發情況時,如資料庫的讀寫時,只要簡單的加個synchronized關鍵字即可,那麼併發的問題就解決了。但是這是最低效的併發方式的處理,也就是不管三七二十一,set和get方法都給加個synchronized就完事了。那麼怎樣才能實現高效併發呢?下面來看下CocurrentHashMap原始碼關於高效併發問題的解決方案,不討論所有原始碼,僅涉及執行緒安全的相關原始碼。

1.CocurrentHashMap高效併發原始碼分析

1.1 volatile關鍵字

不瞭解volatile關鍵字的可以看我收藏的這篇:https://blog.csdn.net/fwt336/article/details/80986409

,對volatile 說的很詳細。

而線上程併發中,我們就需要用到volatile 的可見特性,來保證併發操作變數的可見性,而對於volatile 的非原子操作,我們可以看CocurrentHashMap是怎麼做的。

1.2 volatile的使用

下面來看看CocurrentHashMap原始碼中關於volatile的應用:

我們都知道,在原始碼中是通過這個table陣列來儲存我們存入Map中的key和value值的:

transient volatile Node<K,V>[] table;

而Node是才是真正對我們的key和value值的封裝:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    public final K getKey()     { return key; }
    public final V getValue()   { return val; }
    public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
    public final String toString() {
        return Helpers.mapEntryToString(key, val);
    }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    ...
}

而在Node中有一個next,如果你想問為啥會有一個next節點在這裡呢?那就說明你對Map的儲存細節不夠熟悉,這裡與HashMap是類似的。簡單總結下就是,Map雖然是通過陣列table來儲存資料,但是在table陣列中的Node節點,確是通過連結串列來實現的,因為在儲存的時候會發生hash碰撞,但是不同key可能通過hash換算後所對應table陣列的index是一樣的,所以在陣列中同一個index的值會有多個key和value存在,那麼我們通過連結串列就可以接近這個問題,這也就是為什麼HashMap不是有序儲存的了。而由於資料量太大時,連結串列的查詢效能問題就會很明顯了,這時候會對連結串列進行樹化,來優化效能,而樹化用的是紅黑樹。

扯遠了,回來=======================================================================

我們看到原始碼中的val和next都用volatile修飾了,而且在Node的原始碼中我們也沒有看到synchronized這個同步關鍵字。我們看到get方法也是不需要synchronized關鍵字的,因為有了volatile的可見性來保證資料的可見性操作。那麼它的原子操作又是在哪裡實現的呢?

1.3 原子操作的保證synchronized

現在我們知道通過使用volatile修飾val和next之後,get方法是不需要synchronized來修飾的,這樣效能就得到了一定的提升。

那麼涉及到修改,肯定是在set方法了:

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
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
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            ...
        }
    }
    addCount(1L, binCount);
    return null;
}
我們看到synchronize同步了變數f這個節點,也就是我們需要操作的value。比起我們直接同步一個方法,效能也會大大提高。也就是我們在設定節點,替換節點,清除節點等對value操作時,只需要同步我們操作的這個節點即可。

2.CopyOnWriteArrayList高效併發原始碼分析

同樣的,CopyOnWriteArrayList也使用了volatile來保證可見性,synchronized進行同步,看陣列定義:

private transient volatile Object[] elements;

不同的是:

final transient Object lock = new Object();

還有一個這玩意,lock,沒錯:

public E set(int index, E element) {
    synchronized (lock) {
        Object[] elements = getArray();
        ...
        return oldValue;
    }
}
public boolean add(E e) {
    synchronized (lock) {
        ...
        return true;
    }
}
private boolean remove(Object o, Object[] snapshot, int index) {
    synchronized (lock) {
        ...
        return true;
    }
}

...

在CocurrentHashMap中,synchronized同步的是當前需要操作的Node節點,而這裡使用的是一個Object類例項來作為鎖的物件,所有涉及到對elements陣列的操作都需要先獲取這把鎖。這樣也就達到了執行緒同步的作用。

3.總結

所以,從Java提供的執行緒安全類的原始碼來看,實現高效併發的方式有:

1.對可變欄位使用volatile修飾

2.getXX方法不需要加synchronized關鍵字

3.涉及到變數的所有修改操作,對需要操作的變數使用synchronized關鍵字進行同步,或定義一個Object例項充當鎖,進行同步