目錄
1.2 不安全現象2:ConcurrentModificationException
2.2 不安全現象2:ConcurrentModificationException
1 不安全集合ArrayList
1.1 不安全現象1:元素丟失
測試代碼
for (int i = 0; i < 1000; i ++) {
List<Integer> list = new ArrayList<>();
executor.execute(() -> list.add(1));
executor.execute(() -> list.add(2));
Thread.sleep(100);
System.out.println(list);
}
測試結果:
[1]
[null, 2]
[2]
ArrayList.add源碼如下
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
這裏分析一個比較簡單的併發問題,size ++
線程1和線程2同時執行add方法,其中線程1先執行,給數組中size位置放置好元素後,還沒有自增1,cpu調度到線程2執行,線程2同樣給size處放元素,就導致後執行的線程覆蓋掉前一個線程放置的元素。
1.2 不安全現象2:ConcurrentModificationException
ArrayList迭代器:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
原因:每次add,modCount++,迭代的時候會初始化expectModCount(迭代器變量)=modCount(ArrayList變量)。
如果線程1先獲取迭代器,執行next()的時候停住了線程2添加一個element,那麼modCount發生變化,線程1然後
線程1又開始執行,這個時候checkForComodification就會檢查expectModCount != modCount拋異常。
爲了證明這個問題以及確保線程執行順序,我們對ArrayList源碼進行修改
// 修改add方法
public boolean add(E e) {
try {
// 這裏睡100ms,確保先獲取到迭代器
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size ++] = e;
System.out.println(Thread.currentThread().getName() + "添加元素:" + e);
return true;
}
// 修改iterator方法
public Iterator<E> iterator() {
Itr itr = new Itr();
System.out.println(Thread.currentThread().getName() + "獲取迭代器");
try {
// 這裏睡200ms,確保獲取到迭代器後有添加元素的操作
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return itr;
}
// Itr.next方法添加日誌
public E next() {
System.out.println(Thread.currentThread().getName() + "開始遍歷,checkForComodification");
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
測試代碼
@Test
public void test() throws InterruptedException {
List<Integer> list = new ArrayList<>(4);
executor.execute(() -> list.add(1));
System.out.println(list);
TimeUnit.MINUTES.sleep(1);
}
測試結果如下,100%復現異常
main獲取迭代器
pool-1-thread-1添加元素:1
main開始遍歷,checkForComodification
java.util.ConcurrentModificationException
at com.demo.util.ArrayList$Itr.checkForComodification(ArrayList.java:918)
2 不安全集合HashMap
2.1 不安全現象1:元素覆蓋
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 兩個線程同時走到這裏會發生後一個線程把前一個線程添加的元素給覆蓋掉
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
2.2 不安全現象2:ConcurrentModificationException
HashMap的keySet遍歷機制和ArrayList類似,這裏不再贅述
2.3 不安全現象3:死鎖
該現象在jdk1.7中出現,jdk1.8 hashmap採用尾插法避免了此問題
3 集合線程安全化
那麼如何獲取線程安全的集合呢?可以通過如下幾個方式
1 用Vector,HashTable代替ArrayList和HashMap
2 通過Collections工具類構建線程安全的集合:Collections.synchronizedList(), Collections.synchronizedMap();
上面兩種方式優點就是實現比較簡單,直接用synchronized鎖住方法或者鎖住方法裏的整段代碼。那麼有沒有集合類,既是線程安全的,性能也比傳統的加鎖方式好呢?答案是有的,JUC併發包下有豐富的併發集合,在確保線程安全的同時又可以改善傳統加鎖方式的性能,可以參考筆者JUC併發工具系列博客