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