ArrayList線程安全問題
眾所周知,ArrayList
不是線程安全的,在並發場景使用ArrayList
可能會導致add內容為null,迭代時並發修改list內容拋ConcurrentModificationException
異常等問題。java類庫里面提供了以下三個輪子可以實現線程安全的List,它們是
-
Vector -
Collections.synchronizedList -
CopyOnWriteArrayList
本文簡要的分析了下它們線程安全的實現機制並對它們的讀,寫,迭代性能進行了對比。
Vector
從JDK1.0開始,Vector
便存在JDK中,Vector
是一個線程安全的列表,底層采用數組實現。其線程安全的實現方式非常粗暴:Vector
大部分方法和ArrayList
都是相同的,只是加上了synchronized
關鍵字,這種方式嚴重影響效率,因此,不再推薦使用Vector
了。JAVA官方文檔中這樣描述:
If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.
如果不需要線程安全性,推薦使用ArrayList替代Vector
關鍵源碼如下:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized Iterator<E> iterator() {
return new Itr();
}
可以看到Vector
通過在方法級別上加入了synchronized
關鍵字實現線程安全性。
Collections.synchronizedList
因為ArrayList不是線程安全的,JDK提供了一個Collections.synchronizedList
靜態方法將一個非線程安全的List(並不僅限ArrayList)包裝為線程安全的List。使用方式如下:
List list = Collections.synchronizedList(new ArrayList());
根據文檔,轉換包裝后的list可以實現add,remove,get等操作的線程安全性,但是對於迭代操作,Collections.synchronizedList
並沒有提供相關機制,所以迭代時需要對包裝后的list(敲黑板,必須對包裝后的list進行加鎖,鎖其他的不行)進行手動加鎖,使用方式如下:
List list = Collections.synchronizedList(new ArrayList());
//必須對list進行加鎖
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
這個地方要注意兩個地方:
-
迭代操作必須加鎖,可以使用 synchronized
關鍵字修飾; -
synchronized持有的監視器對象必須是 synchronized (list)
,即包裝后的list,使用其他對象如synchronized (new Object())
會使add
,remove
等方法與迭代方法使用的鎖不一致,無法實現完全的線程安全性。
通過源碼可知Collections.synchronizedList
生成了特定同步的SynchronizedCollection
,生成的集合每個同步操作都是持有mutex
這個鎖,所以再進行操作時就是線程安全的集合了。關鍵地方已經加了注釋:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
//ArrayList使用了SynchronizedRandomAccessList類
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
//SynchronizedRandomAccessList繼承自SynchronizedList
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
}
//SynchronizedList對代碼塊進行了synchronized修飾來實現線程安全性
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
//迭代操作並未加鎖,所以需要手動同步
public ListIterator<E> listIterator() {
return list.listIterator();
}
}
CopyOnWriteArrayList
CopyOnWriteArrayList
是java.util.concurrent
包下面的一個實現線程安全的List,顧名思義, Copy~On~Write~ArrayList在進行寫操作(add,remove,set等)時會進行Copy操作,可以推測出在進行寫操作時CopyOnWriteArrayList
性能應該不會很高。
先看一下 CopyOnWriteArrayList
的結構:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/** * Creates an empty list. */
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
}
可以看到CopyOnWriteArrayList
底層實現為Object[] array
數組。
添加元素:
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();
}
}
可以看到每次添加元素時都會進行Arrays.copyOf
操作,代價非常昂貴。
讀的時候是不需要加鎖的,直接獲取。刪除和增加是需要加鎖的。
有兩點必須講一下。我認為CopyOnWriteArrayList
這個並發組件,其實反映的是兩個十分重要的分布式理念:
(1)讀寫分離
我們讀取CopyOnWriteArrayList
的時候讀取的是CopyOnWriteArrayList
中的Object[] array
,但是修改的時候,操作的是一個新的Object[] array
,讀和寫操作的不是同一個對象,這就是讀寫分離。這種技術數據庫用的非常多,在高並發下為了緩解數據庫的壓力,即使做了緩存也要對數據庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然后讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多。
(2)最終一致
對CopyOnWriteArrayList
來說,線程1讀取集合里面的數據,未必是最新的數據。因為線程2、線程3、線程4四個線程都修改了CopyOnWriteArrayList
里面的數據,但是線程1拿到的還是最老的那個Object[] array
,新添加進去的數據並沒有,所以線程1讀取的內容未必准確。不過這些數據雖然對於線程1是不一致的,但是對於之后的線程一定是一致的,它們拿到的Object[] array
一定是三個線程都操作完畢之后的Object array[]
,這就是最終一致。最終一致對於分布式系統也非常重要,它通過容忍一定時間的數據不一致,提升整個分布式系統的可用性與分區容錯性。當然,最終一致並不是任何場景都適用的,像火車站售票這種系統用戶對於數據的實時性要求非常非常高,就必須做成強一致性的。
性能對比
通過前面的分析可知
-
Vector
對所有操作進行了synchronized
關鍵字修飾,性能應該比較差 -
CopyOnWriteArrayList
在寫操作時需要進行copy
操作,讀性能較好,寫性能較差 -
Collections.synchronizedList
性能較均衡,但是迭代操作並未加鎖,所以需要時需要額外注意
下面寫了個測試程序對三者的讀,寫,遍歷進程了測試來驗證下,測試機器信息如下:
操作系統:macOS High Sierra 10.13.6
CPU:2.8 GHz Intel Core i7
內存:16 GB 2133 MHz LPDDR3
測試代碼:
**
* 比較Vector,Collections.synchronizedList,CopyOnWriteArrayList讀操作,寫操作,遍歷操作性能
*
* @author nauyus
* @date 2020年01月29日
*/
public class ListPerformanceTest {
/** * 並發數 */
public final static int THREAD_COUNT = 64;
/** * list大小 */
public final static int SIZE = 10000;
/** * 測試讀性能 * * @throws Exception */
@Test
public void testGet() throws Exception {
List<Integer> list = initList();
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(list);
List<Integer> synchronizedList = Collections.synchronizedList(list);
Vector vector = new Vector(list);
int copyOnWriteArrayListTime = 0;
int synchronizedListTime = 0;
int vectorTime = 0;
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
copyOnWriteArrayListTime += executor.submit(new GetTestTask(copyOnWriteArrayList, countDownLatch)).get();
}
System.out.println("CopyOnWriteArrayList get method cost time is " + copyOnWriteArrayListTime);
for (int i = 0; i < THREAD_COUNT; i++) {
synchronizedListTime += executor.submit(new GetTestTask(synchronizedList, countDownLatch)).get();
}
System.out.println("Collections.synchronizedList get method cost time is " + synchronizedListTime);
for (int i = 0; i < THREAD_COUNT; i++) {
vectorTime += executor.submit(new GetTestTask(vector, countDownLatch)).get();
}
System.out.println("vector get method cost time is " + vectorTime);
}
private List<Integer> initList() {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < SIZE; i++) {
list.add(new Random().nextInt(1000));
}
return list;
}
class GetTestTask implements Callable<Integer> {
List<Integer> list;
CountDownLatch countDownLatch;
GetTestTask(List<Integer> list, CountDownLatch countDownLatch) {
this.list = list;
this.countDownLatch = countDownLatch;
}
@Override
public Integer call() {
int pos = new Random().nextInt(SIZE);
long start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
list.get(pos);
}
long end = System.currentTimeMillis();
countDownLatch.countDown();
return (int) (end - start);
}
}
完整版代碼可以點擊閱讀原文或公眾號內回復文章編號010
獲取
測試結果:

可以看到隨着線程數的增加,三個類操作時間都有所增加,Vector
的遍歷操作和CopyOnWriteArrayList
的寫操作(圖片中標紅的部分)性能消耗尤其嚴重。出乎意料的是Vector
的讀寫操作和Collections.synchronizedList
比起來並沒有什么差別(印象中Vector
性能很差,實際性能差的只是遍歷操作,看來還是紙上得來終覺淺,絕知此事要躬行啊),仔細分析了下代碼,雖然Vector
使用synchronized
修飾方法,Collections.synchronizedList
使用synchronized
修飾語句塊,但實際鎖住內容並沒有什么區別,性能相似也在情理之中。
總結
-
CopyOnWriteArrayList
的寫操作與Vector
的遍歷操作性能消耗尤其嚴重,不推薦使用。 -
CopyOnWriteArrayList
適用於讀操作遠遠多於寫操作的場景。 -
Vector
讀寫性能可以和Collections.synchronizedList
比肩,但Collections.synchronizedList
不僅可以包裝ArrayList
,也可以包裝其他List,擴展性和兼容性更好。
參考資料:
Java集合:CopyOnWriteArrayList與SynchronizedList
感謝閱讀,如有收獲,求
點贊
、求關注
讓更多人看到這篇文章,本文首發於不止於技術的技術公眾號Nauyus
,歡迎識別下方二維碼獲取更多內容,主要分享JAVA,微服務,編程語言,架構設計,思維認知類等原創技術干貨,2019年12月起開啟周更模式,歡迎關注,與Nauyus一起學習。

福利一:后端開發視頻教程
這些年整理的幾十套JAVA后端開發視頻教程,包含微服務,分布式,Spring Boot,Spring Cloud,設計模式,緩存,JVM調優,MYSQL,大型分布式電商項目實戰等多種內容,關注Nauyus立即回復【視頻教程】無套路獲取。
福利二:面試題打包下載
這些年整理的面試題資源匯總,包含求職指南,面試技巧,微軟,華為,阿里,百度等多家企業面試題匯總。 本部分還在持續整理中,可以持續關注。立即關注Nauyus回復【面試題】無套路獲取。