ArrayList執行緒不安全怎麼辦?
有三種解決方法:
-
使用對應的
Vector
類,這個類中的所有方法都加上了synchronized
關鍵字- 就和 HashMap 和 HashTable 的關係一樣
-
使用 Collections 提供的
synchronizedList
方法,將一個原本執行緒不安全的集合類轉換為執行緒安全的,使用方法如下:List<Integer> list = Collections.synchronizedList(new ArrayList<>());
-
其實 HashMap 也可以用這招:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
-
這個看上去有點東西,其實也是給每個方法加上一個
synchronized
,不過不是直接加在方法上,而是加在方法內部,只有當執行緒獲取到mutex
這個物件的鎖,才能進入程式碼塊:public E get(int index) { synchronized (mutex) { return list.get(index); } }
-
-
使用 JUC 包下提供的
CopyOnWriteArrayList
類- 其實 ConcurrentHashMap 也是 JUC 包下的
這裡具體討論一下 CopyOnWriteArrayList
這個類,它採用了“寫時複製”的技術,也就是說,每當要往這個 list 中新增元素時,並不是直接就新增了,而是會先複製一份 list,然後在這個複製中新增元素,最後再修改指標的指向,看看 add 的原始碼:
public boolean add(E e) {
synchronized (lock) {
//得到當前的陣列
Object[] es = getArray();
int len = es.length;
//複製一份並擴容
es = Arrays.copyOf(es, len + 1);
//把新元素新增進去
es[len] = e;
//修改指標的指向
setArray(es);
return true;
}
}
有人可能會疑惑,這有什麼意義,這不也加了 synchronized
嗎,而且還要複製陣列,這**不是比 Vector 還要爛嗎?
確實是這樣的,在寫操作比較多的場景下,CopyOnWriteArrayList
確實比 Vector
還要慢,但它有兩個優勢:
-
雖然寫操作爛了,但讀操作快了很多,因為在 vector 中,讀操作也是需要鎖的,而在這裡,讀操作就不需要鎖了,get 方法比較短可能不便於理解,我們看看
indexOf
這個方法:public int indexOf(Object o) { Object[] es = getArray(); return indexOfRange(o, es, 0, es.length); } private static int indexOfRange(Object o, Object[] es, int from, int to) { if (o == null) { for (int i = from; i < to; i++) if (es[i] == null) return i; } else { //****here**** for (int i = from; i < to; i++) if (o.equals(es[i])) return i; } return -1; }
- 可以發現,這個方法先把當前陣列 array 交給了 es 這個變數,後續的所有操作都是基於 es 進行的(此時 array 和 es 都指向記憶體中的同一份陣列 a1)
- 由於所有寫操作都是在 a1 的拷貝上進行的(我們把記憶體中的這份拷貝稱為 a2),因此不會影響到那些正在 a1 上進行的讀操作,並且就算寫操作執行完畢了,array 指向了 a2,也不會影響到 es 這個陣列,因為 es 指向的還是 a1
- 試想,如果 vector 的讀操作不加鎖會出現什麼情況?由於 vector 中所有的讀寫操作都是基於同一個陣列的,因此雖然讀操作一開始拿到的陣列是沒問題的,但在後續遍歷的過程中(比如上面程式碼標註了 here 的地方),很可能出現其他執行緒對陣列進行了修改,誇張點說,如果有個執行緒把陣列給清空了,那麼讀操作就肯定會報錯了,而對於
CopyOnWriteArrayList
來說,就算有清空的操作,那也是在 a2 上進行的,而讀操作還是在 a1 上進行,不會有任何影響
-
在 forEach 遍歷一個 vector 時,是不允許對 vector 進行修改的,會報出
ConcurrentModificationException
這個異常,理由很簡單,因為只有一份陣列,要是遍歷到一半有其它執行緒把陣列清空了不就出問題了嗎,因此 java 乾脆就直接禁止這種遍歷時修改陣列的行為了,但對於CopyOnWriteArrayList
來說,它的遍歷是一直在 a1 上進行的,其它寫執行緒只能修改到 a2,這對 a1 是沒有任何影響的,我們看一段程式碼來驗證一下:public class Test { public static void main(String[] args) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 1000; i++) { list.add(i); } //遍歷時把陣列清空 for (Integer i : list) { System.out.println(i); list.clear(); } } }
- 結果是沒有報錯,並且完整輸出了 0~999 所有的數字,可見這裡遍歷的就是最開始的那個陣列 a1,期間哪怕有再多的寫操作也不會影響到 a1,因為所有的寫操作都是在 a2 a3 a4 上進行的
綜上所述,CopyOnWriteArrayList
的優點就是,讀操作很快,不需要鎖,並且支援遍歷,遍歷過程中就算陣列被修改也不會報錯,在讀多寫少的場景下是很優秀的
但它的缺點也很明顯,主要有兩點:
- 首先,寫操作的記憶體消耗非常大,每次修改陣列都會進行一次拷貝,如果陣列比較大或者修改次數比較多,很快就會消耗掉大量記憶體,觸發 GC,因此在寫多的場景下一定要慎用這個類
- 其次,雖然讀操作和遍歷不需要上鎖,也允許遍歷時陣列被修改,但這些操作都是基於舊陣列 a1 的,在它們執行
Object[] es = getArray()
這條語句的一瞬間就決定了它們所要操作的陣列,因此它們是沒有辦法感知到最新的那些資料的,就算途中新增了一個很重要的資料,這個資料也是在 a2 中,遍歷 a1 是無法得到這個資料的