剑指CopyOnWriteArrayList
上期回顾
之前的一篇 剑指ConcurrentHashMap【基于JDK1.8】 给大家详细分析了一波JUC的ConcurrentHashMap,它在线程安全的基础上提供了更好的写并发能力。那么既然有map,是不是还有List呢?并发情况下应该用什么List嘞?接下来就是咋们的主角CopyOnWriteArrayList登场。
简介
首先咋们先看名字,比ArrayList多了个CopyOnWrite。CopyOnWrite啥意思?写时拷贝!OK,CopyOnWriteArrayList精髓你已经知道了,就是写时拷贝!很easy是把。
贴个比较官方的简介
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
先说说ArrayList的缺点
大家都知道ArrayList是属于线程不安全的一个集合,为什么不安全?咋们先看下它的添加操作源码
1 |
public boolean (E e) {
|
很easy,里面有两步操作
- 判断列表的capacity容量是否足够,是否需要扩容
- 真正将元素放在列表的元素数组里面
ensureCapacityInternal()这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。
这里就出现了线程安全问题,注意,add操作实际上不是一个原子操作,是分两步执行的,先扩容,后赋值。
问题出现在elementData[size++] = e这里,它不是一个原子操作,可以分解为elementData[size] = e;和size++。当然,有经验的朋友可能还会说size++也不是原子操作,对,没错,++其实在字节码指令里面被分成了三步操作,不懂的朋友可以参考我之前写的 CAS机制是什么鬼?
看测试案例:

开了20个线程进行add()操作1000次,理论上每次size应该都是20000,但是出现很多不足20000的情况。
由此可见是ArrayList做add操作时候,会丢失一些数据,个别情况还会报java.lang.ArrayIndexOutOfBoundsException错!
值不足20000是典型的ArrayList多线程值覆盖问题。即AB线程同时进行add()方法,假设同时得到size为1,elementData[n]的值被设置了两次,第二个线程设置的值将前一个覆盖,第一个值丢失。
另一种情况是报错,主要是因为扩容引起的。假设一个场景:
list容量为10,当前的size为9。线程AB同时add一个元素,A执行完ensureCapacityInternal(size + 1)后,没达到扩容条件,此时容量还是为4,被挂起。B拿到CPU执行权,也执行了ensureCapacityInternal(size + 1),当然也不会进行扩容操作,接着执行elementData[size++] = e后,B的值添加成功,size变成了4!OK!这时候A拿到CPU了,执行elementData[size++] = e,因为这时size已经是4了,执行elementData[4]就肯定会报数组越界异常了啊!
OK,缺点说完,那多线程情况下咋整呢?
Vector和Collections.synchronizedList
我们知道 Vector是线程安全的容器,因为它大部分方法都是用synchronized关键字确保线程安全,比如add():
1 |
public synchronized boolean (E e) {
|
包括使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全,也和Vector实现方法差不多,比如add():
1 |
public void (int index, E element) {
|
只不过它不是加在方法的声明处,而是方法的内部。
Vector和Collections.synchronizedList可能会出现的问题
有朋友就说了,咋还有问题啊,这是闹啥子啊??别急,听我慢慢道来。
直接贴案例图

我们在遍历Vector的时候,有别的线程修改了Vector的长度,可能会报java.lang.ArrayIndexOutOfBoundsException错误。为什么呢?假设:A线程遍历Vector,发现Vector长度为25,这时候B线程进来了,吧Vector清空了,之后A继续执行vector.get(i)操作的时候,抛出异常。
怎么解决??
很简单,加锁!
1 |
// 遍历Vector |
这样就不会有之前的问题了,但是你随便遍历个集合都要加个synchronized,性能方面肯定大打折扣的,这时候咋们的主角CopyOnWriteArrayList就出来了。
CopyOnWriteArrayList源码分析
看下add()方法:
1 |
public boolean (E e) {
// 加锁
|
通过代码我们可以知道:在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。
再看看set()方法:
1 |
public E set(int index, E element) {
|
和add()方法有异曲同工之妙
总结:
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁
对之前两个案例进行改进:
1 |
public class Test2 {
|
1 |
public class Test {
|
测试运行是没有问题的。
CopyOnWriteArrayList的缺点
啥?CopyOnWriteArrayList还有缺点?ArrayList不行换Vector,Vector不行换CopyOnWriteArrayList,现在CopyOnWriteArrayList咋又不行啦??没完没了了??这代码我不敲了!!!
哈哈,不急不急,稍安勿躁,且听我慢慢道来。
CopyOnWriteArrayList也不是万金油,固然它有许多优点,但是也有两个缺点。
- 内存占用问题
因为CopyOnWrite的写时拷贝机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在拷贝的时候只是拷贝容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,比如ConcurrentHashMap。 - 数据一致性问题
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
注意事项
- 使用CopyOnWriteArrayList应该尽量在创建时指定合适的大小,减少扩容开销,避免写时CopyOnWriteArrayList扩容的开销。
- 尽量使用批量添加方法
addAll(),因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
剑指CopyOnWriteArrayList的更多相关文章
- 剑指Offer面试题:1.实现Singleton模式
说来惭愧,自己在毕业之前就该好好看看<剑指Offer>这本书的,但是各种原因就是没看,也因此错过了很多机会,后悔莫及.但是后悔是没用的,现在趁还有余力,把这本书好好看一遍,并通过C#通通实 ...
- 剑指Offer面试题:14.链表的倒数第k个节点
PS:这是一道出境率极高的题目,记得去年参加校园招聘时我看到了3次,但是每次写的都不完善. 一.题目:链表的倒数第k个节点 题目:输入一个链表,输出该链表中倒数第k个结点.为了符合大多数人的习惯,本题 ...
- 《剑指offer》面试题12:打印1到最大的n位数
面试题12:打印1到最大的n位数 剑指offer题目12,题目如下 输入数字n,按顺序打印出1到最大的n位十进制数,比如输入3,则打印出1,2,3一直到最大的三位数999 方法一 和面试题11< ...
- 《剑指offer》面试题11: 数值的整数次方
面试题11: 数值的整数次方 剑指offer面试题11,题目如下 实现函数double power(double base,int exponent),求base的exponent次方, 不得使用库 ...
- 剑指 Offer 题目汇总索引
剑指 Offer 总目录:(共50道大题) 1. 赋值运算符函数(或应说复制拷贝函数问题) 2. 实现 Singleton 模式 (C#) 3.二维数组中的查找 4.替换空格 ...
- 面试题目——《剑指Offer》
1.把一个字符串转换成整数——<剑指Offer>P29 2.求链表中的倒数第k个结点——<剑指Offer>P30 3.实现Singleton模式——<剑指Offer> ...
- 剑指offer习题集2
1.把数组排成最小的数 class Solution { public: static bool compare(const string& s1, const string& s2) ...
- 剑指offer习题集1
1.打印二叉树 程序很简单,但是其中犯了一个小错误,死活找不到,写代码要注意啊 这里左右子树,要注意是node->left,结果写成root->left vector<int> ...
- 剑指Offer:面试题20——顺时针打印矩阵(java实现)
题目描述: 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数 字,例如,如果输入如下矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1, ...
随机推荐
- Tensorflow学习教程------模型参数和网络结构保存且载入,输入一张手写数字图片判断是几
首先是模型参数和网络结构的保存 #coding:utf-8 import tensorflow as tf from tensorflow.examples.tutorials.mnist impor ...
- 使用PHANTOMJS对网页截屏
PhantomJS 是一个基于 WebKit 的服务器端 JavaScript API.它全面支持web而不需浏览器支持,其快速,原生支持各种Web标准: DOM 处理, CSS 选择器, JSON, ...
- Android之UI View与ViewGroup
1.基本概念 View:所有可视化控件的父类,Android App屏幕上用户可以交互的对象(例如 按钮 下拉框 文本框等). ViewGroup:View的子类,存放View和ViewGroup对象 ...
- Apsara Clouder云计算技能认证:云数据库管理与数据迁移
一.课程介绍 二.云数据库的简介及使用场景 1.云数据库简介 1.1特点: 用户按存储容量和带宽的需求付费 可移植性 按需扩展 高可用性(HA) 1.2阿里云云数据库 RDS 稳定可靠,可弹性伸缩的在 ...
- MySQL数据库高可用集群搭建-PXC集群部署
Percona XtraDB Cluster(下文简称PXC集群)提供了MySQL高可用的一种实现方法.集群是有节点组成的,推荐配置至少3个节点,但是也可以运行在2个节点上. PXC原理描述: 分布式 ...
- Getting Started with STM32 in Segger Embedded Studio
初识Segger Embedded Studio(SES) 第一次见SES是在“安富莱电子论坛”上,“硬汉”提到SES的一些特性,再加上Jlink的大名,于是试试他们家的IDE. SES貌似也是基于E ...
- 二十九、rsync+inotity实时监控同步工具
一.场景应用: 客户通过url访问资源(查询,下载等),并发量是非常高的,所以运用负载均衡分担web服务器的压力,在后端连接不同的 ...
- 6)HTML中a链接跳转地址怎么写
(1)看 thinkphp5的 附录--->助手函数 --->url 利用url进行书写地址跳转: 比如,你想跳转到cate控制器下的lst方法: <a href=" ...
- 20)PHP,数组的遍历
然后开始使用这2个函数和while循环结构来实现数组遍历: 形式: reset($arr1); while ( list ($key, $value ) = each( $arr1) ) //从数组$ ...
- 43)PHP,mysql_fetch_row 和mysql_fetch_assoc和mysql_fetch_array
mysql_fetch_row 提取的结果是没有查询中的字段名了(也就是没有键id,GoodsName,只有值),如下图: mysql_fetch_assoc 提取的结果有键值,如下图: mysq ...