java-并发-并发容器(3)
同樣注意內層的第一個for循環,里面有語句int c = segments[i].count; 但是c卻從來沒有被使用過,即使如此,編譯器也不能做優化將這條語句去掉,因為存在對volatile變量count的讀取,這條語句存在的唯一目的就是保證segments[i].modCount讀取到幾乎最新的值。關于containsValue方法的其它部分就不分析了,它和size方法差不多。
跨段方法中還有一個isEmpty()方法,其實現比size()方法還要簡單,也不介紹了。最后簡單地介紹下迭代方法,如keySet(), values(), entrySet()方法,這些方法都返回相應的迭代器,所有迭代器都繼承于Hash_Iterator類(提交時居然提醒我不能包含sh It,只得加了下劃線),里實現了主要的方法。其結構是:
abstract class Hash_Iterator{ int nextSegmentIndex; int nextTableIndex; HashEntry<K,V>[] currentTable; HashEntry<K, V> nextEntry; HashEntry<K, V> lastReturned; }nextSegmentIndex是段的索引,nextTableIndex是nextSegmentIndex對應段中中hash鏈的索引,currentTable是nextSegmentIndex對應段的table。調用next方法時主要是調用了advance方法:
final void advance() { if (nextEntry != null && (nextEntry = nextEntry.next) != null) return; while (nextTableIndex >= 0) { if ( (nextEntry = currentTable[nextTableIndex--]) != null) return; } while (nextSegmentIndex >= 0) { Segment<K,V> seg = segments[nextSegmentIndex--]; if (seg.count != 0) { currentTable = seg.table; for (int j = currentTable.length - 1; j >= 0; --j) { if ( (nextEntry = currentTable[j]) != null) { nextTableIndex = j - 1; return; } } } } }不想再多介紹了,唯一需要注意的是跳到下一個段時,一定要先讀取下一個段的count變量。
這種迭代方式的主要效果是不會拋出ConcurrentModificationException。一旦獲取到下一個段的table,也就意味著這個段的頭結點在迭代過程中就確定了,在迭代過程中就不能反映對這個段節點并發的刪除和添加,對于節點的更新是能夠反映的,因為節點的值是一個volatile變量。
結束語
ConcurrentHashMap是一個支持高并發的高性能的HashMap實現,它支持完全并發的讀以及一定程度并發的寫。ConcurrentHashMap的實現也是很精巧,充分利用了最新的JMM規范,值得學習,卻不值得模仿。
1.8 的版本,與JDK6的版本有很大的差異。實現線程安全的思想也已經完全變了,它摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實現,利用CAS算法。它沿用了與它同時期的HashMap版本的思想,底層依然由“數組”+鏈表+紅黑樹的方式思想,但是為了做到并發,又增加了很多輔助的類,例如TreeBin,Traverser等對象內部類。CAS算法實現無鎖化的修改值的操作,他可以大大降低鎖代理的性能消耗。這個算法的基本思想就是不斷地去比較當前內存中的變量值與你指定的一個變量值是否相等,如果相等,則接受你指定的修改的值,否則拒絕你的操作。因為當前線程中的值已經不是最新的值,你的修改很可能會覆蓋掉其他線程修改的結果。這一點與樂觀鎖,SVN的思想是比較類似的。
List類型的CopyOnWriteArrayList
對應的非并發容器:ArrayList
目標:代替Vector、synchronizedList
原理:利用高并發往往是讀多寫少的特性,對讀操作不加鎖,對寫操作,先復制一份新的集合,在新的集合上面修改,然后將新集合賦值給舊的引用,并通過volatile 保證其可見性,當然寫操作的鎖是必不可少的了。
CopyOnWriteArrayList采用寫入時復制的方式避開并發問題。這其實是通過冗余和不可變性來解決并發問題,在性能上會有比較大的代價,但如果寫入的操作遠遠小于迭代和讀操作,那么性能就差別不大了。
應用它們的場合通常在數組相對較小,并且遍歷操作的數量大大超過可變操作的數量時,這種場合應用它們非常好。它們所有可變的操作都是先取得后臺數組的副本,對副本進行更改,然后替換副本,這樣可以保證永遠不會拋出ConcurrentModificationException移除。
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,CopyOnWriteArrayList在某些情況下具有更好的性能,考慮讀遠大于寫的場景,如果把所有的讀操作進行加鎖,因為只有一個讀線程能夠獲得鎖,所以其他的讀線程都必須等待,大大影響性能。CopyOnWriteArrayList稱為“寫時復制”容器,就是在多線程操作容器對象時,把容器復制一份,這樣在線程內部的修改就與其他線程無關了,而且這樣設計可以做到不阻塞其他的讀線程。從JDK1.5開始Java并發包里提供了兩個使用CopyOnWrite機制實現的并發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList源碼
先說說CopyOnWriteArrayList容器的實現原理:簡單地說,就是在需要對容器進行操作的時候,將容器拷貝一份,對容器的修改等操作都在容器的拷貝中進行,當操作結束,再把容器容器的拷貝指向原來的容器。這樣設計的好處是實現了讀寫分離,并且讀讀不會發生阻塞。下面的源碼是CopyOnWriteArrayList的add方法實現:
上面的三個步驟實現了寫時復制的思想,在讀數據的時候不會鎖住list,因為寫操作是在原來容器的拷貝上進行的。而且,可以看到,如果對容器拷貝修改的過程中又有新的讀線程進來,那么讀到的還是舊的數據。讀的代碼如下
public E get(int index) {return get(getArray(), index);}final Object[] getArray() {return array;}CopyOnWrite并發容器用于讀多寫少的并發場景。比如白名單,黑名單,商品類目的訪問和更新場景
從CopyOnWriteArrayList的實現原理可以看到因為在需要容器對象的時候進行拷貝,所以存在兩個問題:內存占用問題和數據一致性問題。
內存占用問題
因為需要將原來的對象進行拷貝,這需要一定的開銷。特別是當容器對象過大的時候,因為拷貝而占用的內存將增加一倍(原來駐留在內存的對象仍然在使用,拷貝之后就有兩份對象在內存中,所以增加了一倍內存)。而且,在高并發的場景下,因為每個線程都拷貝一份對象在內存中,這種情況體現得更明顯。由于JVM的優化機制,將會觸發頻繁的Young GC和Full GC,從而使整個系統的性能下降。
數據一致性問題
CopyOnWriteArrayList不能保證實時一致性,因為讀線程在將引用重新指向原來的對象之前再次讀到的數據是舊的,所以CopyOnWriteArrayList只能保證最終一致性。因此在需要實時一致性的廠幾個CopyOnWriteArrayList是不能使用的
CopyOnWriteArrayList適用于讀多寫少的場景
在并發操作容器對象時不會拋出ConcurrentModificationException,并且返回的元素與迭代器創建時的元素是一致的
容器對象的復制需要一定的開銷,如果對象占用內存過大,可能造成頻繁的YoungGC和Full GC
CopyOnWriteArrayList不能保證數據實時一致性,只能保證最終一致性。
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,CopyOnWriteArrayList在某些情況下具有更好的性能,考慮讀遠大于寫的場景,如果把所有的讀操作進行加鎖,因為只有一個讀線程能夠獲得鎖,所以其他的讀線程都必須等待,大大影響性能。CopyOnWriteArrayList稱為“寫時復制”容器,就是在多線程操作容器對象時,把容器復制一份,這樣在線程內部的修改就與其他線程無關了,而且這樣設計可以做到不阻塞其他的讀線程。從JDK1.5開始Java并發包里提供了兩個使用CopyOnWrite機制實現的并發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList容器使用示例
首先啟動5個寫線程,再啟動10個讀線程,運行該程序發現并沒有出現異常,所以使用寫時復制容器效率很高。代碼的運行結果如下:
先說說CopyOnWriteArrayList容器的實現原理:簡單地說,就是在需要對容器進行操作的時候,將容器拷貝一份,對容器的修改等操作都在容器的拷貝中進行,當操作結束,再把容器容器的拷貝指向原來的容器。這樣設計的好處是實現了讀寫分離,并且讀讀不會發生阻塞。下面的源碼是CopyOnWriteArrayList的add方法實現:
上面的三個步驟實現了寫時復制的思想,在讀數據的時候不會鎖住list,因為寫操作是在原來容器的拷貝上進行的。而且,可以看到,如果對容器拷貝修改的過程中又有新的讀線程進來,那么讀到的還是舊的數據。讀的代碼如下:
public E get(int index) {return get(getArray(), index);}final Object[] getArray() {return array;}總結
以上是生活随笔為你收集整理的java-并发-并发容器(3)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: XML 和 JSON 的使用场景
- 下一篇: 廖雪峰javascript教程学习记录