執行緒併發--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 的非原子操作,我們可以看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例項充當鎖,進行同步