多線程5-同步容器和並發容器
同步容器出現的原因?
在Java的集合容器框架中,主要四大類是List、Set、Queue、Map。其中List、Set、Queue分別繼承了Collection頂層接口,Map本身是一個頂層接口。我們常用的ArrayList、LinkedList、HashMap這些容器都是非線程安全的,如果有多個線程並發訪問這些容器時,就會出現問題。因此,編寫程序時,必須要求開發者手動在任何訪問到這些容器的地方進行同步處理,這樣導致使用這些容器時的不便,所以,Java提供了同步容器供用戶使用。
Java中的同步容器類:
1、Vector、Stack、HashTable
2、Collections類中提供的靜態工廠方法創建的類
Vector實現了List接口,其實際上就是一個類似於ArrayList的數組,但Vector中的方法都是synchronized方法。Stack也是同步容器,實際上Stack是繼承於Vector。HashTable也進行了同步處理,但HashMap沒有。Collections類是一個工具提供類,其中包含對集合或容器進行排序、查找等操作的方法。重要的是,它也提供了幾個靜態工廠方法來創建同步容器類。
同步容器存在的缺陷:
1、同步容器中的方法采用了synchronized進行同步,這會影響執行性能。
2、同步容器不一定是真正完全的線程安全,同步容器中的方法是線程安全的,但對這些集合類的符合操作無法保證其線程安全性,仍舊需要通過主動加鎖來保證。
3、ConcurrentModificationException異常
Vector等容器叠代時同時對其修改,會報ConcurrentModificationException異常。
ConcurrentModificationException異常詳解:
public class Test { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(2); Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==2) list.remove(integer); } } }
//結果
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.testdemo.demo.Test.main(Test.java:20)
執行上述代碼後,會報該異常。並發現錯誤發生在checkForComodification()方法中,我們不直接看該方法內容。先了解一下ArrayList中itreator方法的具體實現。該方法存在於父類AbstractList中。
public Iterator<E> iterator() {
return new Itr();
}
該方法返回一個指向Itr類型對象的引用。看看其具體實現。
private class Itr implements Iterator<E> {
//表示下一個要訪問的元素的索引 int cursor = 0;
//表示上一個要訪問的元素的索引 int lastRet = -1;
//expectedModCount表示對ArrayList修改次數的期望值,初始值為modCount
//modCount是AbstractList類中的一個成員變量。表示對List的修改次數。每次調用add或remove方法都會對modCount進行+1操作 int expectedModCount = modCount;
//判斷是否還有元素未被訪問
public boolean hasNext() { return cursor != size(); }
//獲取到下標為0的元素 public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } //如果 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
首先調用list.iterator()返回一個Iterator後,通過hashNext方法判刑是否還有元素未被訪問。該方法通過對比下一個訪問的元素下標和ArrayList的大小。不等於時說明還有元素需要訪問。
然後通過next方法獲取到下標為0的元素。該方法中先調用checkForComodification()方法,根據cursor的值獲取到元素。將cursor值賦給lastRet。初始時,cursor為0,lastRet為-1.調用一次後curosr的值為1,lastRet為0.此時modCount為0,expectedModCount也為0。
當元素值為2時,調用list.remove()方法。我們看一下ArrayList中remove方法的源碼。
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
remove方法刪除元素實際是調用fastRemove()方法,在該方法中,首先對modCount進行加1,表示對集合修改了一次。然後刪除元素,最終將size進行減1,並將引用設置為null方便垃圾回收。此時對於iterator,expectedModCount為0,cursor為1,lastRet為0。對於list,其modCount為1,size為0。當執行完刪除操作,繼續while循環。調用hasNext()方法。此時的cursor為1,size為0,返回true繼續執行循環,調用next()方法。next方法首先調用checkForComodification()對比modCount和expectedModCount。此時值不一樣,拋出ConcurrentModificationException異常。關鍵之處在於:list.remove會導致modCount和expectedModCount不一致。
ConcurrentModificationException異常解決:
單線程:單線程情況下,采用叠代器提供的remove方法進行刪除就不會拋出異常。
多線程:在使用iterator叠代的時候,使用synchronized或lock同步、使用並發容器CopyOnWriteArrayList代替ArrayList和Vector
並發容器
同步容器將所有對容器狀態的訪問都串行化了,保證線程安全性的同時嚴重降低了並發性,當多個線程競爭容器時,吞吐量嚴重降低。從JDK5開始針對多線程並發訪問設計,提供了並發性能較好的並發容器,引入了java.util.concurrent 包。
與Vector和Hashtable、Collection.synchronizedXxx()同步容器等相比,util.concurrent中的並發容器主要解決了兩個問題:
1、根據具體場景設計,盡量避免synchronized,提供並發性
2、定義一些並發安全的復合操作,並且保證並發環境下的叠代操作不會出錯。util.concurrent中容器在叠代時,可以不封裝在synchronized中,可以保證不拋異常,但未必每次看到的都是最新的數據。
並發容器簡單介紹:
ConcurrentHashMap代替同步的Map。HashMap是根據散列值分段存儲,同步Map在同步時鎖住了所有的段,而ConcurrentHashMap加鎖的時候根據散列值鎖住了散列值對應的那段,提高了並發性能。ConcurrentHashMap提供了對常用符合操作的支持。比如"若沒有則添加":putIfAbsent(),替換:replace()。這2個操作都是原子操作。
HashMap和ConcurrentHashMap的區別: 1、HashMap不是線程安全的,而ConcurrentHashMap是線程安全的。 2、ConcurrentHashMap采用鎖分段技術,將整個Hash桶進行了分段segment,也就是將這個大的數組分成了幾個小的片段segment,而且每個小的片段segment上面都有鎖存在,那麽在插入元素的時候就需要先找到應該插入到哪一個片段segment,然後再在這個片段上面進行插入,而且這裏還需要獲取segment鎖。 3、ConcurrentHashMap可以做到讀取數據不加鎖,並且其內部的結構可以讓其在進行寫操作的時候能夠將鎖的粒度保持地盡量地小,不用對整個ConcurrentHashMap加鎖,並發性能更好。
CopyOnWriteArrayList和CopyOnWriteArraySet分別代替List和Set。主要是在遍歷操作為主的情況下代替同步的List和同步的Set。
ConcurrentLinedQueue是一個先進先出的非阻塞隊列。
ConcurrentSkipListMap可以在高效並發中替代SoredMap、ConcurrentSkipListSet可以在高效並發中替代SoredSet。
多線程5-同步容器和並發容器