一、写在开头

我们在学习集合或者说容器的时候了解到,很多集合并非线程安全的,在并发场景下,为了保障数据的安全性,诞生了并发容器,广为人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你们知道ArrayList也有自己对应的并发容器嘛?

作为使用频率最高的集合类之一,ArrayList线程不安全,我们在并发环境下使用,一般要辅以手动上锁、或者通过Collections.synchronizedList()转一手,为了解决这一问题,Doug Lea(道格.利)大师为我们提供了它的并发类——CopyOnWriteArrayList

二、认知CopyOnWriteArrayList

CopyOnWriteArrayList 是java.util.concurrent的并发类,线程安全,遵循写时复制的原则(CopyOnWrite),什么意思呢?就是我们在对列表进行增删改时,会先创建一个列表的副本,在副本中完成增删改操作后,再将副本替换原列表,整个过程旧的列表并没有锁定,因此原来的读取操作仍可继续。

看到这里细心的同学应该已经发现了它的“弊端”了,先赋值副本,写完再替换,这是有时间差的,没错,这就是CopyOnWrite的延时更新策略,我们在发生写的同时,不阻塞读,但读取的只是旧列表中的数据,直到引用替换完成,可以保证数据的最终一致性,无法保证实时性。

三、实现原理(源码)

我们来看一下CopyOnWriteArrayList底层的源码实现,首先在内部维护了一个数组,用volatile关键字修饰,保证了数据的内存可见性

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

读取:get()方法

public E get(int index) {
return get(getArray(), index);
}
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}

这段源码没什么,很好理解,就是普通的读取数组的操作,这也能看出CopyOnWriteArrayList的读是不阻塞的。

新增:add()方法

public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1. 使用Lock,保证写线程在同一时刻只有一个
lock.lock();
try {
//2. 获取旧数组引用
Object[] elements = getArray();
int len = elements.length;
//3. 创建新的数组,并将旧数组的数据复制到新数组中
Object[] newElements = Arrays.copyOf(elements, len + 1);
//4. 往新数组中添加新的数据
newElements[len] = e;
//5. 将旧数组引用指向新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}

通过这段源码,我们就能够感知到前面描述的实现原理了,首先,新增元素时,内部通过可重入锁进行锁定,说明写时会独占,然后,再将原数组赋值到一个新数组中,最后,将旧数组的引用指向新数组。

四、使用注意事项,用不好坑死你

对于CopyOnWriteArrayList的日常使用,和ArrayList几乎一模一样,在这里就不用过多介绍了,但它的使用还是需要注意的,虽然可以保证线程安全,但因其特性所致,仅适应于读多写少的并发环境,对于频繁写入或者写入的对象较大,一定不要使用CopyOnWriteArrayList容器,不然会坑死你的!

【举个例子】

之前在这篇文章中:[EasyExcel导入导出百万数据量]

采用了CopyOnWriteArrayList,以此来保证在多线程写入数据库时的线程安全,由于写入的excel文件中有100万的数据量,再导入的时候非常之慢,用了514秒!

核心实现代码如下,具体内容实现可去看那篇文章哈

@Slf4j
@Service
public class EasyExcelImportHandler implements ReadListener<User> {
/*成功数据*/
private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>();
/*单次处理条数*/
private final static int BATCH_COUNT = 20000;
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@Resource
private UserMapper userMapper; @Override
public void invoke(User user, AnalysisContext analysisContext) {
if(StringUtils.isNotBlank(user.getName())){
successList.add(user);
return;
}
if(successList.size() >= BATCH_COUNT){
log.info("读取数据:{}", successList.size());
saveData();
} } /**
* 采用多线程读取数据
*/
private void saveData() {
List<List<User>> lists = ListUtil.split(successList, 20000);
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<User> list : lists) {
threadPoolExecutor.execute(()->{
try {
userMapper.insertSelective(list.stream().map(o -> {
User user = new User();
user.setName(o.getName());
user.setId(o.getId());
user.setPhoneNum(o.getPhoneNum());
user.setAddress(o.getAddress());
return user;
}).collect(Collectors.toList()));
} catch (Exception e) {
log.error("启动线程失败,e:{}", e.getMessage(), e);
} finally {
//执行完一个线程减1,直到执行完
countDownLatch.countDown();
}
});
}
// 等待所有线程执行完
try {
countDownLatch.await();
} catch (Exception e) {
log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e);
}
// 提前将不再使用的集合清空,释放资源
successList.clear();
lists.clear();
} /**
* 所有数据读取完成之后调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
//读取剩余数据
if(CollectionUtils.isNotEmpty(successList)){
log.info("读取数据:{}条",successList.size());
saveData();
}
}
}

而将这段代码中的CopyOnWriteArrayList换为ArrayList。

/*成功数据*/
// private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>();
private final List<User> successList = new ArrayList<>();

导入100万数据量的耗时,直接从分钟降为秒级,由此可见CopyOnWriteArrayList在写入大对象时的性能非常之差!

五、总结

通过以上的学习,我们进行总结:CopyOnWriteArrayList的优势在于可以保证线程安全的同时,不阻塞读操作,但是这仅限于读多写少的情况;

在写多读少的情况下,或者写入的对象占用内容较大时,不建议使用CopyOnWriteArrayList;CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器,最好通过 ReentrantReadWriteLock 自定义一个的列表。

六、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

Doug Lea大师的佳作CopyOnWriteArrayList,用不好能坑死你!的更多相关文章

  1. Java并发大师Brain Goetz和Doug Lea 的中英文博客文章地址

    Java并发大师Brain Goetz和Doug Lea是Java并发方面最权威的人物,他的文章绝对是最具有参考价值的,值得仔仔细细的推敲和研究. Brain Goetz 中文地址:http://ww ...

  2. Doug Lea

    如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea.这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷. ...

  3. 学习 Doug Lea 大神写的——Scalable IO in Java

    学习 Doug Lea 大神写的--Scalable IO in Java 网络服务 Web services.分布式对象等等都具有相同的处理结构 Read request Decode reques ...

  4. Doug Lea在J.U.C包里面写的BUG又被网友发现了

    这是why的第 69 篇原创文章 BUG描述 一个编号为 8073704 的 JDK BUG,将串联起我的这篇文章. 也就是下面的这个链接. https://bugs.openjdk.java.net ...

  5. Fork/Join 框架-设计与实现(翻译自论文《A Java Fork/Join Framework》原作者 Doug Lea)

    作者简介 Dong Lea任职于纽约州立大学奥斯威戈分校(State University of New York at Oswego),他发布了第一个广泛使用的java collections框架实 ...

  6. kafka和zookeeper安装部署(版本弄不好就是坑)

    yum install -y unzip zip 配置host vi /etc/host172.19.68.10 zk1 1. zookeeper zookeeper下载地址 http://mirro ...

  7. 卧槽,redis分布式如果用不好,坑真多

    前言 在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中. 但不是说用了redis分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引 ...

  8. 16.并发容器之CopyOnWriteArrayList

    1. CopyOnWriteArrayList的简介 java学习者都清楚ArrayList并不是线程安全的,在读线程在读取ArrayList的时候如果有写线程在写数据的时候,基于fast-fail机 ...

  9. java程序员最不愿意看到的十件事

     0.遍历结果集并构造对象如果你是个时髦的开发者而不是专业人员,显然你从某篇博客中读过有开发者遇到Hibernate的“性能问题”,因而认为ORM都不好,觉得手动编码“明显更好”.喜欢的话你当然可以用 ...

  10. 不愿看到Java开发者再做的10件事

    William F. Buckley.Jr 曾经说过,“保守主义者是那些逆着历史潮流不断喊停的人,其他人都不愿意这么做或者对他们这么做显得没有耐性”.虽然我对此了解不多,但是每次看到有Java开发人员 ...

随机推荐

  1. MySQL 数据库查询与数据操作:使用 ORDER BY 排序和 DELETE 删除记录

    使用 ORDER BY 进行排序 使用 ORDER BY 语句按升序或降序对结果进行排序. ORDER BY 关键字默认按升序排序.要按降序排序结果,使用 DESC 关键字. 示例按名称按字母顺序排序 ...

  2. 报表的 SQL 注入风险是什么意思?如何防范?

    啥是 SQL 注入风险? 数据库要执行 SQL 访问数据,数据库是个执行机构,它只会检查传来的 SQL 是不是合乎语法,而并不会关心这个语句是否会造成伤害(数据泄露或破坏).正因为只要符合语法规则就会 ...

  3. 润乾报表如何从 mongodb 中取数

    MongoDB 属于 NoSql 中的基于分布式文件存储的文档型数据库,是非关系数据库当中功能最丰富,最像关系数据库的.它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较 ...

  4. 从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁

    写在开头 随手一翻,发现对于Java中并发多线程的学习已经发布了十几篇博客了,多线程 是Java基础中的重中之重!因此,可能还需要十几篇博客才能大致的讲完这部分的知识点,初学者对于这部分内容一定要多花 ...

  5. 【Oracle】在PL/SQL中使用sql实现选择排序

    [Oracle]在PL/SQL中使用sql实现选择排序 一般来说,SQL要排序的话直接使用order by即可 不一般来说,就是瞎搞,正好也可以巩固自己的数据结构基础 使用SQL实现排序系列: 使用S ...

  6. Oracle 查询超级慢之buffer sort

    查询超级慢之buffer sort 在视图中增加了一个临时表作为一个数据源进行id和名称的转换,没加的时候一秒不到,加了以后14秒,感觉有点问题,于是打开了解释计划看了下,发现这个buffer sor ...

  7. Helm Chart 多环境、多集群交付实践,透视资源拓扑和差异

    简介: 在本文中,我们将介绍如何通过 KubeVela解决多集群环境下 Helm Chart 的部署问题.如果你手里没有多集群也不要紧,我们将介绍一种仅依赖于 Docker 或者 Linux 系统的轻 ...

  8. RDS PostgreSQL一键大版本升级技术解密

    ​简介: 内容简要: 一.PostgreSQL行业位置 二.PostgreSQL版本升级背景 三.PostgreSQL版本升级解密 四.PostgreSQL版本升级成果 一.PostgreSQL行业位 ...

  9. 庖丁解牛-图解MySQL 8.0优化器查询解析篇

    ​简介: SQL优化器本质上是一种高度抽象化的数据接口的实现,经过该设计,客户可以使用更通用且易于理解的SQL语言,对数据进行操作和处理,而不需要关注和抽象自己的数据接口,极大地解放了客户的应用程序. ...

  10. [FAQ] VsCode 同时多行编辑

      [多光标自定义编辑] 按住 alt 键,鼠标左键点击,出现多个光标. [批量选中匹配编辑] 鼠标选中字符,按快捷键 ctrl + shift + L,字符的右侧都出现光标,可同时编辑. Tool: ...