![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SY5QGN2MGOhFzN1IGNkFmY3AzN0MTM5EmYxImZ4EzYx8CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
你好,我是看山。
書接上文,上次聊了聊 在多線程中使用 ArrayList 會發生什麼,這次我們說說平時常用的清單:Vector、ArrayList、CopyOnWriteArrayList、SynchronizedList。
Vector
Vector是在 JDK 1.0 提供的,雖然沒有被标記Deprecated,但是事實上已經沒人使用了。主要原因是性能差,且不符合需求。
從源碼可以看出(這裡不貼源碼了),Vector是基于數組實作,幾乎在所有操作方法上,都用synchronized關鍵字實作方法同步,這種同步方式可以對單一操作進行加鎖,比如多個線程同時執行add會同步阻塞執行,但是多線程執行add和remove時,就不會阻塞了。
但是,大部分需要對隊列加鎖的場景,是想對整個隊列加鎖,而不僅僅是對單一操作加鎖。也就是說,Vector和我們的期望不同,但是又額外增加了同步操作帶來的性能開銷。是以,不是必須使用的場景,都可以使用ArrayList代替,即使是多線程情況下需要同步隊列,也可以使用CopyOnWriteArrayList和SynchronizedList代替。
ArrayList
ArrayList是在 JDK 1.1 提供的,作為Vector的繼任者(ArrayList實作方式與Vector幾乎完全相同),ArrayList把方法上的synchronized全部去掉了,完全沒有實作同步,是非線程安全的。
它的非線程安全,還展現在疊代器的快速失敗上。在使用方法iterator和listIterator建立疊代器之後,如果還對原來的ArrayList隊列進行修改(add 或 remove),疊代器疊代的時候就會報ConcurrentModificationException異常。從源碼可以看出,疊代器在疊代過程中,會檢查隊列中修改次數modCount與建立疊代器時落下的修改次數快照expectedModCount是否相等,相等表示沒有修改過,代碼如下:
private class Itr implements Iterator<E> {
// 這段代碼是從 ArrayList 中摘取的
// 隻留下檢查方法,略過其他代碼,有興趣的可以從源碼中檢視
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
第三點是在多線程場景中,添加元素可能會丢失資料,或者發生數組越界異常,在多線程中使用 ArrayList 會發生什麼 有較長的描述,這裡就不贅述了。
SynchronizedList
SynchronizedList是Collections的靜态内部類,使用Collections.synchronizedList()靜态方法建立,是一個通過組合List類實作的封裝實作。它的大多數方法通過synchronized (mutex){...}代碼塊同步方式,因為加鎖對象mutex是隊列對象中定義的相同對象,是以對mutex加鎖時,就實作對整個隊列加鎖,也就解決了Vector不能對整個隊列加鎖的問題。是以如果有多個線程同時操作add和remove方法,會阻塞同步執行。
ArrayList中存在的疊代器快速失敗情況,依然存在,正如下面源碼中的注釋:想要使用疊代器,需要使用者手動實作同步。
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
// 代碼摘自 Collections,省略很多代碼
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
}
手動同步的時候需要注意,既然我們關注的全局同步,在疊代器設定同步的時候,要保證鎖定對象與add等方法中對象相同。這個在後續補充說明,這裡就不展開了。
CopyOnWriteArrayList
CopyOnWriteArrayList是從 JDK 1.5 開始提供的,先看看add方法的源碼:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
// 代碼摘自 CopyOnWriteArrayList,省略很多代碼
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public boolean addAll(Collection<? extends E> c) {
Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
if (cs.length == 0)
return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len == 0 && cs.getClass() == Object[].class)
setArray(cs);
else {
Object[] newElements = Arrays.copyOf(elements, len + cs.length);
System.arraycopy(cs, 0, newElements, len, cs.length);
setArray(newElements);
}
return true;
} finally {
lock.unlock();
}
}
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
}
可以看到,CopyOnWriteArrayList借助ReentrantLock實作同步,在synchronized優化之前,ReentrantLock性能高于synchronized。CopyOnWriteArrayList也是通過數組實作的,但是在數組前面增加了volatile關鍵字,實作了多線程情況下數組的可見性,更加安全。更重要的一點是,CopyOnWriteArrayList在add添加元素的時候,實作方式是重建數組對象,替換原來的數組引用。與ArrayList的擴容方式相比,減少了空間,但是也增加了指派數組的性能開銷。在get擷取元素的時候,沒有任何鎖,直接資料傳回。
CopyOnWriteArrayList的疊代器時通過COWIterator實作的,調用iterator方法時,将目前隊列中數組的快照指派到疊代器中的數組引用上。如果原來的隊列發生修改,隊列中數組會指向别的引用,而疊代器中的數組不會發生變化,是以在多線程執行過程中,通過疊代器周遊數組,也可以修改隊列中的資料。這種方式保障線程安全的同時,也可能會出現資料不一緻的情況,隻能是使用的使用多注意了。
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
}
對比 CopyOnWriteArrayList 和 SynchronizedList
CopyOnWriteArrayList和SynchronizedList都實作了同步,實作方式上采用的是不同政策,各自的側重點不同。
CopyOnWriteArrayList側重于讀寫分離,發生資料寫操作(add或remove)時,會加鎖,各個線程阻塞執行,執行過程會建立資料副本,替換對象引用;如果同時有讀操作(get或iterator),讀操作讀取的是老資料,或者成為曆史資料快照,或者成為緩存資料。這會造成讀寫同時發生時,資料不一緻的情況,但是資料最終會一緻。這種方式與資料庫讀寫分離模式幾乎相同,很多特性可以類比。
SynchronizedList側重資料強一緻,也就是說當發生資料寫操作(add或remove)時,會加鎖,各個線程阻塞執行,而且也會通過相同的鎖阻塞get操作。
從CopyOnWriteArrayList和SynchronizedList兩種不同僚項方式,可以推斷CopyOnWriteArrayList在寫少讀多的場景中執行效率高,SynchronizedList的讀寫操作效率很均衡,是以在寫多讀多、寫多讀少的場景執行效率都會高于CopyOnWriteArrayList。借用網上的測試結果:
文末總結
synchronized關鍵字在 JDK 8 之前性能比較差,可以看到 JDK1.5 之後實作的同步代碼,很多是通過ReentrantLock實作的。
多線程場景中除了需要考慮同步外,還需要考慮資料可見性,可以通過volatile關鍵字實作。
ArrayList完全沒有同步操作,是非線程安全的
CopyOnWriteArrayList和SynchronizedList屬于線程安全隊列
CopyOnWriteArrayList實作讀寫分離,适合場景是寫少讀多的場景
SynchronizedList要求資料強一緻,是隊列全局加鎖方式,讀操作也會加鎖
Vector隻是在疊代器周遊性能很差,如果不考慮全局鎖定隊列,單純讀操作和單獨寫操作性能與SynchronizedList相差不大。
參考
Why is Java Vector (and Stack) class considered obsolete or deprecated?
CopyOnWriteArrayList 與 Collections.synchronizedList 的性能對比
Collections.synchronizedList 、CopyOnWriteArrayList、Vector 介紹、源碼淺析與性能對比
推薦閱讀
如果非要在多線程中使用 ArrayList 會發生什麼?