1. 程式人生 > 實用技巧 >Java5執行緒併發庫之同步集合

Java5執行緒併發庫之同步集合

>>> hot3.png

同步集合

接下來講,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) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;

e = e.next) {
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.

144025_HtlK_3512041.png

Java5以後,提供了各種集合相關的同步類。如果你要用map,它提供了併發的HashMap,名叫ConcurrentHashMap.

還提供了ConcurrentSkipListMap, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet.等同步集合。 下面我們來介紹一下這幾個類:

ConcurrentSkipListMap

ConcurrentSkipListMap:它實現了SortedMap介面,它是一個排序的map,就是說,往這個map裡面存的東西是有順序的,排序要看比較規則。排序一定要傳一個比較器進去的,告訴它排序的比較規則是什麼。該map可以根據鍵的自然順序進行排序,也可以根據建立對映時所提供的 Comparator 進行排序,具體取決於使用的構造方法。這個就類似於TreeMap. 但是是執行緒安全的。

ConcurrentSkipListSet

ConcurrentSkipListSet:set 的元素可以根據它們的自然順序進行排序,也可以根據建立 set 時所提供的 Comparator 進行排序,具體取決於使用的構造方法。這個就類實於TreeSet,但是是執行緒安全的。

CopyonWriteArrayListCopyonWriteArraySet

接下來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