同步容器類
同步容器類包括Vector和Hashtable(兩者是早期JDK的一部分),還包括JDK1.2中添加的一些類似的類。同步容器類實現線程安全的方式是:將狀態封閉起來,並對每一個公有方法進行同步,使得每次只有一個線程能訪問容器狀態。這裏解釋一下所謂「狀態」指的就是成員變量,「封裝起來」即將它們設不private,可是經過公有的方法外界仍然能夠訪問修改類的私有成員,因此要用synchronized將公有方法進行同步,使得每次只有一個線程能訪問容器狀態。在多線程環境下調用同步容器類自帶的全部方法時,實際上都是在串行執行,因此這嚴重下降併發性和吞吐量。 java
像List、Set、Map這些本來不是同步容器類,也能夠經過Collections.synchronizedXXX工廠方法將其變爲同步容器類,即對其公有方法進行同步。 數組
- List<Student> students=Collections.synchronizedList(new ArrayList<Student>());
同步容器類的問題
容器上常見的操做包括:迭代訪問、跳轉(根據指定順序找到當前元素的下一個元素)、條件運算(先檢查再操做Check-And-Act,好比若沒有則添加)。 安全
- public static Object getLast(Vector vec){
- int lastIndex=vec.size()-1;
- return vec.get(lastIndex);
- }
- public static Object deleteLast(Vector vec){
- int lastIndex=vec.size()-1;
- return vec.remove(lastIndex);
- }
大線程的環境下,getLast()函數中第一行代碼以後第二行代碼以前若是執行了deleteLast(),那麼
getLast()繼續執行第二行就會拋出ArrayIndexOutOfBoundException。因此要把getLast()和deleteLast()都變成原子操做:
- public static Object getLast(Vector vec){
- synchronized(vec){
- int lastIndex=vec.size()-1;
- return vec.get(lastIndex);
- }
- }
- public static Object deleteLast(Vector vec){
- synchronized(vec){
- int lastIndex=vec.size()-1;
- return vec.remove(lastIndex);
- }
- }
又好比容器上的迭代操做:
- for(int i=0;i<vec.size();i++)
- doSomething(vec.get(i));
在size()以後get()以前,其餘線程可能刪除了vec中的元素,一樣會致使拋出
ArrayIndexOutOfBoundException。但這並不意味着Vector不是線程安全的,Vector的狀態仍然是有效的,而拋出的異常也與其規範保持一致。
正確的作法是在迭代以前對vector加鎖: 多線程
- synchronized(vec){
- for(int i=0;i<vec.size();i++)
- doSomething(vec.get(i));
- }
迭代器與ConcurrentModificationException
使用for或for-each循環對容器進行迭代時,javac內部都會轉換成使用Iterator。在對同步容器類進行迭代時若是發現元素個數發生變化,那麼hasNext和next將拋出ConcurrentModificationException,這被稱爲及時失效(fail-fast)。 併發
- List<Student> students=Collections.synchronizedList(new ArrayList<Student>());
- //可能拋出ConcurrentModificationException
- for(Student student:students)
- doSomething(student);
爲了防止拋出ConcurrentModificationException,須要在迭代以前對容器進行加鎖,可是若是doSomething()比較耗時,那麼其餘線程都在等待鎖,會極大下降吞吐率和CPU的利用率。不加鎖的解決辦法是「克隆」容器,在副本上進行迭代,因爲副本被封閉在線程內,其餘線程不會在迭代期間對其進行修改。克隆的過程也須要對容器加鎖,開發人員要作出權衡,由於克隆容器自己也有顯著的性能開銷。
隱藏的迭代器
注意,下面的狀況會間接地進行迭代操做,也會拋出ConcurrentModificationException: app
- 容器的toString()、hashCode()和equals()方法
- containsAll()、removeAll()、retainAll()等方法
- 調用以上方法的方法,好比StringBuildre.append(Object)會調用toString()
- 容器做爲另外一個容器的元素或者鍵
- 把容器做爲參數的構造函數
扯句不相關的話,編譯器會把字符串的鏈接操做轉換爲調用StringBuildre.append(Object)。《Effective Java》也提出當使用多個"+"進行字符串鏈接時考慮使用StringBuildre.append()代替,由於每次「+」操做都要徹底地拷貝2個字符串,而StringBuilder.append()是在第一個字符串後面鏈接第2個字符串,跟C++中的Vector是一個原理。
併發容器
同步容器將對狀態的訪問都串行化,以實現他們的線程安全性。這樣作的代價是嚴重下降了併發性和吞吐量。
併發容器類提供的迭代器不會拋出ConcurrentModificationException,所以不須要在迭代時對容器進行加鎖。
下面介紹Java5.0中新增長的幾個併發容器類。
併發Map
同步容器類在執行每一個操做期間都加了一個鎖,在一些操做中例如HashMap.get()可能包含大量的工做,如何hashCode不能均勻地分佈散列值,那就須要在不少元素上調用equals,而equals自己就有必定的計算量。
HashMap將每一個方法都在同一個鎖上同步使得每次只能有一個線程訪問容器,ConcurrentHashMap與HashMap不一樣,它採用了粒度更細的分段鎖(Lock Striping)。
結果是任意的讀線程能夠併發地訪問Map,讀線程和寫線程能夠併發地訪問Map,必定數量的寫線程能夠併發地訪問Map。並且在單線程的環境下,ConcurrentHashMap比HashMap性能損失很小。
對於須要在整個Map進行的計算,例如size和isEmpty,ConcurrentHashMap會返回一個近似值而非精確值,由於size()返回的值在計算時可能已通過期了。
ConcurrentHashMap接口中增長了對一些常見覆合操做的支持,例如「若沒有則添加」、「若相等則替換」、「若相等則移除」等等,在ConcurrentMap接口中已經聲明爲原子操做。
Copy-On-Write容器
CopyOnWriteArrayList用於替代同步List,同理CopyOnWriteAeeaySet用於替代同步Set,這裏就以
CopyOnWriteArrayList爲例。
「寫時複製(Copy-On-Write)」容器的線程安全性在於:只要發佈一個事實不可變的對象,那麼在訪問對象時就不須要再進一步同步。當須要修改對象的時候都會從新複製發佈一個新的容器副本。
若是一個對象在被建立以後其狀態就不能被修改,那這個對象就是不可變對象。不可變對象必定是線程安全的。但要注意即便將全部域聲明爲final,這個對象仍然有多是可變的,由於final域中能夠保存可變對象的引用。下面舉個例子,在可變對象上構建不可變類:
- public final class ThreeStorage{
- private final Set<String> storages=new HashSet<String>();
- public ThreeStorage(){
- storages.add("One");
- storages.add("Two");
- storages.add("Three");
- }
- public boolean isStorage(String name){
- return storages.contains(name);
- }
- }
雖然Set對象是可變的,但從ThreeStorage的設計上來看,Set對象在構造完成後沒法對其進行修改。
每當須要修改容器時都是複製底層數組,看一下set()函數的的源代碼:
- public class CopyOnWriteArrayList<E>{
- private volatile transient Object[] array;
- final Object[] getArray() {
- return array;
- }
- final void setArray(Object[] a) {
- array = a;
- }
- public E set(int index, E element) {
- //獲取重入鎖
- final ReentrantLock lock = this.lock;
- lock.lock();
- try {
- Object[] elements = getArray();
- Object oldValue = elements[index];
- //使用的是==而非equals
- if (oldValue != element) {
- int len = elements.length;
- //複製底層數組
- Object[] newElements = Arrays.copyOf(elements, len);
- newElements[index] = element;
- //把底層數組寫回
- setArray(newElements);
- } else {
- setArray(elements);
- }
- return (E)oldValue;
- } finally {
- //釋放鎖
- lock.unlock();
- }
- }
- }
一進入set()函數就加了鎖,函數結束時才釋放鎖。
由於複製底層數組自己就有必定的開銷,因此僅當迭代操做遠遠多於修改操做時,才應該使用CopyOnWrite容器。
併發Queue
基本的Collection就是List、Set和Map,Queue底層是由LinkedList來實現的,由於它去除了List的隨機訪問功能,所以更高效。
ConcurrentinkedQueue是一個傳統的先進先出隊列。PriorityQueue是一個非併發的優先隊列(說這個彷佛與本文的主題無關)。
Queue上的操做是阻塞的,若是隊列爲空,那麼獲取元素的操做會當即返回空值。BlockingQueue的插入和獲取操做是阻塞式的,若是隊列爲空,那麼獲取操做將一直阻塞隊列中有一個可用的元素;若是隊列已滿,那麼插入操做將一直阻塞。
Sorted容器
ConcurrentSkipListMap用於替代SortedMap,ConcurrentSkipListSet用於替代SortedSet。TreeMap和TreeSet分別實現了SortedMap和SortedSet。By the way,既然是"sorted",那麼這類容器的元素就必須實現Comparable接口。