集合類不安全的問題
1. ArrayList的執行緒不安全問題
1.1 首先回顧ArrayList底層
- ArrayList的底層資料結構是陣列
- 底層是一個
Object[] elementData
的陣列,初始化預設為空陣列 - 預設容量
DEFAULT_CAPACITY
為10,如果容量不夠呼叫grow()
方法,將容量調整為原來的1.5倍,核心程式碼為int newCapacity = oldCapacity + (oldCapacity >> 1);
- 擴容過程是首先創建出來一個新陣列,之後使用
Arrays.copyOf(elementData, newCapacity)
將原陣列內容拷貝到新陣列
回到本節知識,
1.2 為什麼說ArrayList會存線上程不安全問題?
很簡單的一個示例程式碼:
package com.yuxue.juc.collection;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},"Thread"+ i).start();
}
}
}
此時程式會報異常:
出現了一個java.util.ConcurrentModificationException
異常!
為什麼會出現這樣的結果?首先我們都知道,在呼叫ArrayList時,底層add方法是沒有加synchronized即沒有加鎖的,當不同的執行緒呼叫方法時,會出現不安全的問題
1.3 為什麼會出現這種問題?
併發修改這個list所導致的
一個執行緒正在寫入,另一個執行緒來搶奪,導致資料不一致,併發修改異常
1.4 解決方案?
1.4.1 Vector
- Vector底層加了鎖,加鎖資料一致性一定可以保證,但是併發性急劇下降!
- ArrayList就是犧牲執行緒安全性才提出的
- 但是Vector是在JDK1.0已經出現的,ArrayList在JDK1.2版本出現的
- 所以用Vector可以但是效率太低,那麼有沒有其他的工具類可以滿足?
1.4.2 synchronizedList
將程式碼改為以下的程式碼即可
List<String> list = Collections.synchronizedList(new ArrayList<>());
1.4.3 CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();//寫時複製,讀寫分離
CopyOnWriteArrayList.add
方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//獲取原來的陣列,儲存副本為elements
Object[] elements = getArray();
//獲取原陣列長度
int len = elements.length;
//將其拷貝到新陣列newElements,長度為原陣列加1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//第len上元素為新新增值e
newElements[len] = e;
//設定新陣列為newElements
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite容器即寫時複製(一種讀寫分離的思想),往一個元素新增容器的時候,不直接往當前容器Object[]
新增,而是先將當前容器 Object[]
進行copy,複製出一個新的容器Object[] newElements
,讓後新的容器新增元素,新增完元素之後,再將原 容器的引用指向新的容器setArray(newElements),
這樣做可以對CopyOnWrite容器進行併發的讀,而不需要加鎖, 因為當前容器不會新增任何元素,所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器
2. Set的執行緒不安全問題
HashSet執行緒不安全,同樣報錯為java.util.ConcurrentModificationException
異常!
解決的類或者方法:
Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
其底層還是CopyOnWriteArrayList
,因為其原始碼為:
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
HashSet的底層是HashMap!
/**
* Constructs a new, empty set; the backing HashMap instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
初始容量為16,預設負載因子為0.75的標準的HashMap!
其中底層還有一個很重要的問題,當HashSet呼叫add(e)
方法是,如果是HashMap,其Key為e,value值為什麼?此時通過原始碼我們可以得到:
public boolean add(E e) {
//key為e,value為PRESENT
return map.put(e, PRESENT)==null;
}
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
3. Map的執行緒不安全問題
HashMap是執行緒不安全的,想解決可以用
- Hashtable
- ConcurrentHashMap
- Collections.synchronizedMap
3.1 HashMap和Hashtable區別?
HashMap和Hashtable都實現了Map介面,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:執行緒安全性,同步(synchronization),以及速度
- HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)
- HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是執行緒安全的,多個執行緒可以共享一個Hashtable;而如果沒有正確的同步的話,多個執行緒是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴充套件性更好
- 另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別
- 由於Hashtable是執行緒安全的也是synchronized,所以在單執行緒環境下它比HashMap要慢。如果你不需要同步,只需要單一執行緒,那麼使用HashMap效能要好過Hashtable。
- HashMap不能保證隨著時間的推移Map中的元素次序是不變的
3.2HashMap與ConcurrentHashMap區別?
準備回頭寫一篇部落格專門總結,先留著 : )