lkxsnow
## 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回复【面试题】无套路获取。

分类:

技术点:

相关文章: