为什么线程安全的 List 推荐使用 CopyOnWriteArrayList,而不是 Vector
注:本系列文章中用到的 jdk 版本均为
java8
相比很多同学在刚接触 Java 集合的时候,线程安全的 List 用的一定是Vector
。但是现在用到的线程安全的 List 一般都会用CopyOnWriteArrayList
,很少有人再去用Vector
了,至于为什么,文章中会具体说到。接下来,我们先来简单分析以下Vector
的源码。
一、Vector 集合源码简析
由于本文的重点不是Vector
集合,因此只是简单的分析一下Vector
的初始化方法和添加元素的方法。
Vector
的底层实现和ArrayList
一样,都是由数组实现的。
Vector
的主要变量如下:
1.1 Vector 初始化
Vector
的初始化提供了三个方法,除了可以指定初始容量的大小,还可以指定扩容容量的大小。构造器分别如下:
无参构造器
指定初始化容量的构造器
指定初始化容量和扩容容量大小的构造器
从上面的构造器中可以看出,如果调用无参构造器,则会创建一个初始化容量为10
,扩容容量为0
的Vector
集合。
1.2 如何扩容
Vector
的扩容机制和ArrayList
的很像,如果不清楚ArrayList
的扩容机制,可以看看这篇文章。这里我们直接看Vector
的扩容方法grow
。
通过上面的方法,我们可以看出,如果指定了扩容容量的大小则扩容的新数组大小为原来的数组加上扩容容量的大小,如果不指定扩容容量的大小则扩容的新数组大小为原来数组大小的2
倍。这样扩容为原来的 2 倍是很消耗空间的,这也是Vector
被弃用的原因之一。
除此之外,看过源码的同学可能发现了,Vector
集合的所有操作元素的方法都加了synchronized
关键字,这就导致了操作Vector
的效率会非常低,在开发中,往往读操作的使用频率会远高于其他操作,而CopyOnWriteArrayList
就是这样一种读操作效率远高于写操作效率的 List,一起来看看。
二、CopyOnWriteArrayList 源码简析
CopyOnWriteArrayList
类图:
2.1 CopyOnWrite 思想
CopyOnWrite
简称 COW,根据名字来看就是写入时复制。意思就是大家共同去访问一个资源,如果有人想要去修改这个资源的时候,就需要复制一个副本,去修改这个副本,而对于其他人来说访问得资源还是原来的,不会发生变化。
2.2 初始化 CopyOnWriteArrayList
CopyOnWriteArrayList
底层是也是有数组实现的。 本文我们只解读添加元素和读取元素的区别,删除修改元素原理和添加元素差不多,操作时都需要进行加锁,而读操作不会加锁。
CopyOnWriteArrayList
主要有以下两个变量:
我们仔细来分析一下上面两个属性,这两个思想是 CopyOnWriteArrayList
的核心 。
lock:ReentrantLock,独占锁,多线程运行的情况下,只有一个线程会获得这个锁,只有释放锁后其他线程才能获得。
array:存放数据的数组,关键是被
volatile
修饰了,被volatile
修饰,就保证了可见性,也就是一个线程修改后,其他线程立即可见。
最常用的初始化方式如下:
初始化只是创建了一个空的数组,并将array
指向它。
2.3 添加元素
添加数组的步骤如下:
获得独占锁,将添加功能加锁
获取原来的数组,并得到其长度
创建一个长度为原来数组长度+1 的数组,并拷贝原来的元素给新数组
追加元素到新数组末尾
指向新数组
释放锁
这个过程是线程安全的,COW 的核心思想就是每次修改的时候拷贝一个新的资源去修改,add()
方法再拷贝新资源的时候将数组容量+1,这样虽然每次添加元素都会浪费一定的空间,但是数组的长度正好是元素的长度,也在一定程度上节省了扩容的开销。
2.4 获取元素
读操作是天然安全的操作,而且数组本身会进行检查越界问题,因此获取元素的方法很简单,只是根据索引获取该元素。
由于CopyOnWriteArrayList
的底层数组长度,本身就是元素大小,因此size()
方法只要返回数组长度就可以了。
三、总结
Vector
和CopyOnWriteArrayList
都是线程安全的 List,底层都是数组实现的,Vector
的每个方法都进行了加锁,而CopyOnWriteArrayList
的读操作是不加锁的,因此CopyOnWriteArrayList
的读性能远高于Vector
,Vector
每次扩容的大小都是原来数组大小的2
倍,而CopyOnWriteArrayList
不需要扩容,通过 COW 思想就能使数组容量满足要求。两个集合都是先了RandomAccess
接口,支持随机读取,因此更加推荐使用 for 循环进行遍历。在开发中,读操作会远远多于其他操作,因此使用CopyOnWriteArrayList
集合效率更高。
点关注、不迷路
如果觉得文章不错,欢迎关注、点赞、收藏,你们的支持是我创作的动力,感谢大家。
如果文章写的有问题,请不要吝惜文笔,欢迎留言指出,我会及时核查修改。
如果你还想看到更多别的东西,可以微信搜索「Java 旅途」进行关注。「Java 旅途」目前已经整理各种中间件的使用教程及各类 Java 相关的面试题。扫描下方二维码进行关注就可以得到这些资料。
版权声明: 本文为 InfoQ 作者【Java旅途】的原创文章。
原文链接:【http://xie.infoq.cn/article/f03b819103dbbc0cb2aae3f03】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论