Java5執行緒併發庫之同步集合
同步集合
接下來講,java5中提供的同步集合。傳統的集合在併發訪問時是有問題的。比如HashSet,HashMap,ArrayList,像這樣的類,如果是多個執行緒在操作,多個執行緒在往它裡面取資料,放資料,會出問題的。它們是執行緒不安全的,會把它裡面的資料搞得亂七八糟的。
ConcurrentHashMap
下面我們來看一個關於HashMap同步的問題:
Race Condition引起的效能問題 Race Condition(也叫做資源競爭),是多執行緒程式設計中比較頭疼的問題。特別是Java多執行緒模型當中,經常會因為多個執行緒同時訪問相同的共享資料,而造成資料的不一致性。為了解決這個問題,通常來說需要加上同步標誌“synchronized”,來保證資料的序列訪問。但是“synchronized”是個效能殺手,過多的使用會導致效能下降,特別是擴充套件性下降,使得你的系統不能使用多個CPU資源。 這是我們在效能測試中經常遇見的問題。 可是上個星期我卻遇見了相反的情況:因為缺少同步標誌也同樣會使效能受影響。 那是一個ERP系統,執行在我們的T2000伺服器(8核32執行緒)上。當500個併發使用者的時候居然把所有的CPU都壓得滿滿的(90%以上的忙碌)。這是很少有的現象,在我測試的所有專案中很少有擴充套件性這麼好的系統能把T2000的32個執行緒都佔滿的。我狠狠的誇了他們的應用。話音沒落,卻發現測試結果很差,平均響應時間很長。不可能呀,所有的CPU都在幹活,而且都在使用者態(如果在系統態幹太多的活就有問題了),結果怎麼還會差呢。CPU都在幹嘛呢? 通過工具發現(Dtrace for Java),我們發現很多的CPU都在做一件事情,那就是不停的執行一條Java語句(HashMap.get())。象是進入了死迴圈。我們進行了進一步試驗,讓併發使用者數量為1,不停的執行10分鐘,結果沒有發現這種情況;接著我們讓50個併發使用者同時執行,但是隻執行在一個CPU上(通過psrset),結果也沒有出現死迴圈狀態。只要併發使用者數量超過10個,執行的CPU超過兩個,不到2分鐘就出現死迴圈。一旦死迴圈出現,大量CPU資源被白白浪費,效能自然很差。 通過上面的試驗我們可以很肯定的判斷,是由於併發控制不好,導致資料的不一致,引起的死迴圈。值得一提的是,HashMap不是一個執行緒安全的資料結構,要用到多個執行緒中去,需要自己加上同步標誌,為什麼會死迴圈呢,看看下面HashMap中get函式的原始碼: public V get(Object key) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } get函式會根據key的hashcode來鎖定多個物件,並且遍歷這些物件來找到key所對應的物件。當多個執行緒不安全的修改HanshMap資料結構的時候,有可能使得這個函式進入死迴圈。 我們建議客戶使用ConcurrentHashMap或在使用HanshMap的時候加上同步標誌,問題得到解決! |
在以前沒有併發庫的時候,沒有ConcurrentHashMap的時候,人們是怎麼解決map的同步問題的呢?人們用的是這個方法Collections.synchronizedMap(null);引數是一個Map<K,V> m,作用是返回由指定map支援的同步(執行緒安全的)map。他得到的是一個new SynchronizedMap<>(m);這個SynchronizedMap物件對Map做了裝飾,把所有的方法中的程式碼都用synchronized程式碼塊包起來了。物件鎖是this,當有一個執行緒呼叫了這個物件裡面的任何方法,其它的執行緒就不可以再呼叫這個物件裡面的任何方法了,因為鎖已經被再執行的執行緒拿走了,其它執行緒只能阻塞,等待鎖,拿到鎖後再操作。你原來的map是執行緒不安全的,我返回的map是執行緒安全的。當然,當有了java5的併發庫之後,我們不再建議使用這個工具方法,而是建議使用ConcurrentHashMap,因為它內部做了優化,減小了鎖的粒度,提高了多執行緒併發訪問的執行效率。它將hashMap分為了若干個Segment<K, V>段,我們知道,hashMap內部其實就是一個數組Entry<K,V>[] table,那麼,我們拿一個數組實現hashMap,對hashMap物件加鎖的時候,就會對那個大陣列加鎖,每次只有一個執行緒可以進去操作這個陣列,如果我用多個數組去維持一個hashmap,每一次進去,我們只對這個陣列的一部分進行加鎖,這樣就減小了鎖的粒度。ConcurrentHashMap會維護若干個Segment,每一個Segment都可以理解成是一個小的hashMap,它裡面就會獲得hashMap的Entry(表),做同步操作的時候,是先定位到這個Segment,然後鎖定這一個Segment,執行put,如果有多個執行緒要來操作,比如說有兩個執行緒,那麼這兩個執行緒分別定位到Segment1和Segment2,那麼,這時候它們之間的操作是互不影響的。它們可以同時做這個操作,而不需要進行等待,這個競爭相對來說也就小了很多。
這裡講個題外話,討論一下HashMap和HashSet的關係,HashSet是單列的,HashMap是雙列的,有Key和value.在底層一點,HashMap和HashSet之間的關係,其實HashSet內部的實現用的就是一個HashMap。只是它只是用了HashMap的Key,就夠了,我這個value部分從來不考慮,我從來不使用它。就把HashMap完全可以當作HashSet用。Key不能重複,完全符合HashSet的要求。所以說,HashSet內部使用的是HashMap.
Java5以後,提供了各種集合相關的同步類。如果你要用map,它提供了併發的HashMap,名叫ConcurrentHashMap.
還提供了ConcurrentSkipListMap
, ConcurrentSkipListSet
, CopyOnWriteArrayList
, CopyOnWriteArraySet
.等同步集合。 下面我們來介紹一下這幾個類:
ConcurrentSkipListMap
ConcurrentSkipListMap
:它實現了SortedMap介面,它是一個排序的map,就是說,往這個map裡面存的東西是有順序的,排序要看比較規則。排序一定要傳一個比較器進去的,告訴它排序的比較規則是什麼。該map可以根據鍵的自然順序進行排序,也可以根據建立對映時所提供的 Comparator
進行排序,具體取決於使用的構造方法。這個就類似於TreeMap. 但是是執行緒安全的。
ConcurrentSkipListSet
ConcurrentSkipListSet
:set 的元素可以根據它們的自然順序進行排序,也可以根據建立 set 時所提供的 Comparator
進行排序,具體取決於使用的構造方法。這個就類實於TreeSet,但是是執行緒安全的。
CopyonWriteArrayList
和CopyonWriteArraySet
接下來CopyOnWriteArrayList
, CopyOnWriteArraySet
這兩個要好好介紹一下。
再說這兩個類之前,我們先看一下另外一個問題,就是說,我們說再java5以前的某些集合是執行緒不安全的,除了執行緒不安全,他還有另外一個隱患,在集合迭代的過程中不可以執行remove操作,在讀的過程中不能進行寫操作。當我們執行集合的迭代器的next方法時,會檢查一個變數modCount,我們把它當作一個版本號,它會去檢查這個版本號是否等於expectedModCount預期的版本號的值,如果不相等會丟擲ConcurrentModificationException的異常,當我們剛獲得迭代器物件的時候,它們二者是相等的,但是當我們呼叫了集合的增加,刪除,刪除所有等修改集合資料的方法之後,modCount的值就會++,因此它的值就改變了。modCount的值等於集合的操作次數,集合操作了幾次,這個modelCount就等於幾。也就是說,在迭代集合的過程中,不能對集合進行修改。修改的話,後果很嚴重。當然,如果我們呼叫的時迭代器的remove方法是不會有問題的,程式正常執行。
public class CollectionModifyExceptionTest { public static void main(String[] args) { Collection users = new ArrayList(); users.add(new User("張三", 28)); users.add(new User("李四", 25)); users.add(new User("王五", 31)); Iterator itrUsers = users.iterator();//當得到迭代器的時候,這時modelCount等於3 while (itrUsers.hasNext()) { System.out.println("aaaa"); User user = (User) itrUsers.next();//當迴圈回來檢查時modelCount與預期的值不符合,拋異常 if ("張三".equals(user.getName())) { users.remove(user);//執行了這次remove操作之後,此時modelCount等於4 // itrUsers.remove();//呼叫迭代器的remove方法時沒有問題的,不會拋異常,正常執行 } else { System.out.println(user); } } } } |
public class User implements Cloneable { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof User)) { return false; } User user = (User) obj; // if(this.name==user.name && this.age==user.age) if (this.name.equals(user.name) && this.age == user.age) { return true; } else { return false; } } public int hashCode() { return name.hashCode() + age; } public String toString() { return "{name:'" + name + "',age:" + age + "}"; } public Object clone() { Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException e) { } return object; } public void setAge(int age) { this.age = age; } public String getName() { return name; } } |
接下來我們要解決集合在迭代的時候不能修改的問題,怎麼解決呢?
這裡我們把ArrayList物件換成CopyOnWriteArrayList,就可以了。它在我們對集合進行寫操作的時候會保留一份拷貝,在remove操作時,它會建立一個新的陣列物件,這個新陣列物件的引用賦給了集合的陣列成員變數,這個新陣列中移除了要移除的元素。也就是說,其實它並不擔心一邊迭代,一邊執行寫操作。它迭代的是一個不變的物件,而寫操作會保留到另一個物件。然後它又是支援併發的。它允許迭代的時候修改集合,並且是執行緒安全的。在不能或不想進行同步遍歷,但又需要從併發執行緒中排除衝突時,它也很有用。“快照”風格的迭代器方法在建立迭代器時使用了對陣列狀態的引用。此陣列在迭代器的生存期內不會更改,因此不可能發生衝突,並且迭代器保證不會丟擲 ConcurrentModificationException。建立迭代器以後,迭代器就不會反映列表的新增、移除或者更改。在迭代器上進行的元素更改操作(remove、set 和 add)不受支援。這些方法將丟擲 UnsupportedOperationException。
public class CollectionModifyExceptionTest { public static void main(String[] args) { Collection users = new CopyOnWriteArrayList(); // new ArrayList(); users.add(new User("張三", 28)); users.add(new User("李四", 25)); users.add(new User("王五", 31)); Iterator itrUsers = users.iterator();//當得到迭代器的時候,這時modelCount等於3 while (itrUsers.hasNext()) { System.out.println("aaaa"); User user = (User) itrUsers.next();//當迴圈回來檢查時modelCount與預期的值不符合,拋異常 if ("張三".equals(user.getName())) { users.remove(user);//執行了這次remove操作之後,此時modelCount等於4 // itrUsers.remove();//這裡就不能呼叫迭代器的remove方法了,否則會丟擲異常 } else { System.out.println(user); } } } } |
轉載於:https://my.oschina.net/kangxi/blog/1822504