## 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

关键源码如下:

```java
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 iterator() {
return new Itr();
}

```

可以看到`Vector`通过在方法级别上加入了`synchronized`关键字实现线程安全性。

## Collections.synchronizedList

因为ArrayList不是线程安全的,JDK提供了一个`Collections.synchronizedList`静态方法将一个非线程安全的List(并不仅限ArrayList)包装为线程安全的List。使用方式如下:

```java
List list = Collections.synchronizedList(new ArrayList());
```
根据文档,转换包装后的list可以实现add,remove,get等操作的线程安全性,但是对于迭代操作,`Collections.synchronizedList`并没有提供相关机制,所以迭代时需要对包装后的list(敲黑板,必须对包装后的list进行加锁,锁其他的不行)进行手动加锁,使用方式如下:
```java
List list = Collections.synchronizedList(new ArrayList());
//必须对list进行加锁
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
```

这个地方要注意两个地方:

1. 迭代操作必须加锁,可以使用`synchronized`关键字修饰;
2. synchronized持有的监视器对象必须是`synchronized (list)`,即包装后的list,使用其他对象如`synchronized (new Object())`会使`add`,`remove`等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。

通过源码可知`Collections.synchronizedList`生成了特定同步的`SynchronizedCollection`,生成的集合每个同步操作都是持有`mutex`这个锁,所以再进行操作时就是线程安全的集合了。关键地方已经加了注释:
```java
public static List synchronizedList(List list) {
return (list instanceof RandomAccess ?
//ArrayList使用了SynchronizedRandomAccessList类
new SynchronizedRandomAccessList(list) :
new SynchronizedList(list));
}
//SynchronizedRandomAccessList继承自SynchronizedList
static class SynchronizedRandomAccessList extends SynchronizedList implements RandomAccess {
}

//SynchronizedList对代码块进行了synchronized修饰来实现线程安全性
static class SynchronizedList extends SynchronizedCollection implements List {
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 listIterator() {
return list.listIterator();
}
}

```

## CopyOnWriteArrayList
`CopyOnWriteArrayList`是`java.util.concurrent`包下面的一个实现线程安全的List,顾名思义,
Copy~On~Write~ArrayList在进行写操作(add,remove,set等)时会进行Copy操作,可以推测出在进行写操作时`CopyOnWriteArrayList`性能应该不会很高。

先看一下 `CopyOnWriteArrayList` 的结构:

```java
public class CopyOnWriteArrayList
implements List, 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`数组。

添加元素:

```java
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
```

### 测试代码:

```java
**
* 比较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 list = initList();
List copyOnWriteArrayList = new CopyOnWriteArrayList(list);
List 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 initList() {
List list = new ArrayList();
for (int i = 0; i {
List list;
CountDownLatch countDownLatch;

GetTestTask(List 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 感谢阅读,如有收获,求`点赞`、求`关注`让更多人看到这篇文章,本文首发于不止于技术的技术公众号 `Nauyus` ,欢迎识别下方二维码获取更多内容,主要分享JAVA,微服务,编程语言,架构设计,思维认知类等原创技术干货,2019年12月起开启周更模式,欢迎关注,与Nauyus一起学习。

![](https://tva1.sinaimg.cn/large/006tNbRwly1gbfkbff49kj30bu0cwdl5.jpg)

#### 福利一:后端开发视频教程
这些年整理的几十套JAVA后端开发视频教程,包含微服务,分布式,Spring Boot,Spring Cloud,设计模式,缓存,JVM调优,MYSQL,大型分布式电商项目实战等多种内容,关注Nauyus立即回复【视频教程】无套路获取。

#### 福利二:面试题打包下载

这些年整理的面试题资源汇总,包含求职指南,面试技巧,微软,华为,阿里,百度等多家企业面试题汇总。
本部分还在持续整理中,可以持续关注。立即关注Nauyus回复【面试题】无套路获取。

Collections.synchronizedList 、CopyOnWriteArrayList、Vector介绍、源码浅析与性能对比的更多相关文章

  1. 【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

    HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 Array ...

  2. Java 集合系列 05 Vector详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  3. ArrayList、LinkedList和Vector的源码解析,带你走近List的世界

    java.util.List接口是Java Collections Framework的一个重要组成部分,List接口的架构图如下: 本文将通过剖析List接口的三个实现类——ArrayList.Li ...

  4. Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  5. Java 集合系列 10 Hashtable详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  6. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  7. Java 集合系列07之 Stack详细介绍(源码解析)和使用示例

    概要 学完Vector了之后,接下来我们开始学习Stack.Stack很简单,它继承于Vector.学习方式还是和之前一样,先对Stack有个整体认识,然后再学习它的源码:最后再通过实例来学会使用它. ...

  8. Java 集合系列11之 Hashtable详细介绍(源码解析)和使用示例

    概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习.我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable.第1部分 Ha ...

  9. HashSet其实就那么一回事儿之源码浅析

    上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap,  本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...

随机推荐

  1. JQ绑定事件的叠加和解决,index()方法的坑

    JQ绑定事件的叠加和解决,index()方法的坑 前言 在做过几个不大不小的项目后,发现技术这种东西,必须要多多实践,才能发现各种问题,理论的知识掌握的再好终究是纸上谈兵. 因此目前感觉有两点是必须要 ...

  2. 023.MFC_属性页控件(tab control)

    属性页控件属性页->选项卡->对话框CTabCtrl一.建立名为tabCtrl的mfc工程,添加Tab Control控件,设置属性ID为IDC_TAB,并添加变量m_tab 在tabCt ...

  3. Vijos1917 艾酱最喜欢的数字 [求绝对众数]

    1.题意:第一行一个数字N,表示一共有多少个数字,第二行N个数字,保证其中至少有一个数字出现次数超过一半,任务是求出这个出现最多的数. 2.分析:本题是明显的求众数的问题,常规思路为开一个大数组,在读 ...

  4. 前端——css属性方法

    目录 标签快捷写法 宽和高 字体 1.文字字体 2.字体大小 3.字体粗细 4.字体颜色 5.字体对齐 6.文字下划线控制 7.文字首行缩进 8.字体英文换大写 背景属性 1.背景颜色 2.背景图片 ...

  5. java_字段初始化的规律、静态方法中访问类的实例成员、查询创建对象的个数

    字段初始化规律: 当执行如下代码时 class InitializeBlockClass{ public int field=100; { field=200; } public Initialize ...

  6. 使用easyExcel遇到的坑

    最近有个功能,用easyExcel代替poi ,这个确实方便了不少,但是使用easyExcel也踩到了很多坑,在这里记录下easyExcel存在的问题,希望阅读这篇文档的人,可以更好的避免这些. 1. ...

  7. layui treeSelect

    官方地址:https://fly.layui.com/extend/treeSelect/ 下面介绍一下这个插件的使用方法 1.html页面 <div class="layui-inp ...

  8. 如何选择API测试工具

    没有最好,只有最合适. 如今,越来越多的公司正在向DevOps的方向左转,以实现持续集成和持续部署开发.这意味着我们的反馈需要比以往更快,以便确定我们的应用程序是否准备好交付.这就是API测试如此重要 ...

  9. Go网络编程

    概述 网络协议 从应用的角度出发,协议可理解为"规则",是数据传输和数据的解释的规则.假设,A.B双方欲传输文件.规定: 第一次,传输文件名,接收方接收到文件名,应答OK给传输方: ...

  10. 阿里云函数计算 .NET Core 初体验

    体验了一波阿里云函数计算, 已支持 .NET Core 2.1, 那么按照惯例, 来写个 "Hello World" 吧. 作者注: 开发环境 Windows 10 & V ...