集合類不安全的問題

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是執行緒不安全的,想解決可以用

  1. Hashtable
  2. ConcurrentHashMap
  3. Collections.synchronizedMap

3.1 HashMap和Hashtable區別?

HashMap和Hashtable都實現了Map介面,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:執行緒安全性,同步(synchronization),以及速度

  1. HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)
  2. HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是執行緒安全的,多個執行緒可以共享一個Hashtable;而如果沒有正確的同步的話,多個執行緒是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴充套件性更好
  3. 另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別
  4. 由於Hashtable是執行緒安全的也是synchronized,所以在單執行緒環境下它比HashMap要慢。如果你不需要同步,只需要單一執行緒,那麼使用HashMap效能要好過Hashtable。
  5. HashMap不能保證隨著時間的推移Map中的元素次序是不變的

3.2HashMap與ConcurrentHashMap區別?

準備回頭寫一篇部落格專門總結,先留著 : )