## 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. TextInputLayout低版本bug :“android.view.InflateException: Binary XML file line #6 : Error inflating class Textview”

    开发中用到TextInputLayout配合TextInputEdittext做输入框,在android7.0 android8.0手机上运行正常,在异步android5.0.2的手机上,点击输入框就 ...

  2. 安装低版本Microsoft .NET Framework 4.5受阻解决方案

    在VS目标框中找不到Microsoft .NET Framework 4.5,项目出错,安装受阻.... 1.Microsoft .NET Framework 安装了高版本后,低版本通过网上上下载的d ...

  3. IPv4数据报格式及其语义

    一.IP数据报的格式如下图所示 版本 首部长度 服务类型 数据报长度 16比特标识 标志 13比特片偏移 寿命 上层协议 首部检验和 32比特源IP地址 32比特目的IP地址 选项(如果有的话) 数据 ...

  4. Python正则表达式之findall疑点

    在findall中使用()进行分组时,得出的结果会优先提取分组的,比如下面这个例子 In [46]: re.findall(r"www.(baidu|163).com", &quo ...

  5. 专业版12.0试用,打开演示账套,提示&ldquo;列名FPlatver无效&rdquo;

    你好,我代表研发来说明一下这个问题:一.产生的原因:由于KIS产品今年陆续都增加了应用平台,前期平台兼容性还有些问题,如果一台电脑有多个不同版本的平台,就会产生冲突,因此报错.1.可能您的机器装了多个 ...

  6. Webpack4不求人系列(1)

    Webpack是一个现在Javascript应用程序的模块化打包器,在Webpack中JS/CSS/图片等资源都被视为JS模块,简化了编程.当Webpack构建时,会递归形成一个模块依赖关系图,然后将 ...

  7. 从壹开始 [ Ids4实战 ] 之七 ║ 客户端、服务端、授权中心全线打通

    1.经过元旦两天的全力整改,终于在这新的一年,完成了我的布道生涯的第一个大步走 —— 那就是客户端(VUE).服务端(ASP.NET Core API).授权中心(IdentityServer4)的大 ...

  8. [工具] Git版本管理(四)(贡献开源代码、git配置、git免密、gitignore)

    一.开源项目贡献代码 1.fork项目代码 例如,我们想向tornado框架贡献代码,首先搜索tornado. 然后,将tornado的代码fork到我们的仓库中. 2.clone到本地进行开发 克隆 ...

  9. CSP201803-4棋局评估

    问题描述 Alice和Bob正在玩井字棋游戏. 井字棋游戏的规则很简单:两人轮流往3*3的棋盘中放棋子,Alice放的是“X”,Bob放的是“O”,Alice执先.当同一种棋子占据一行.一列或一条对角 ...

  10. 【题解】BZOJ4548 小奇的糖果(树状数组)

    [题解]BZOJ4548 小奇的糖果(树状数组) 说在前面:我有个同学叫小奇,他有一个朋友叫达达,达达特爱地理和旅游,初中经常AK地理,好怀恋和他已经达达一起到当时初中附近许多楼盘的顶楼逛的时光... ...