Doug Lea大师的佳作CopyOnWriteArrayList,用不好能坑死你!

简介: 【5月更文挑战第14天】Doug Lea大师的佳作CopyOnWriteArrayList,用不好能坑死你!

一、写在开头

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

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

二、认知CopyOnWriteArrayList

image.png

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秒!

image.png

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

@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在写入大对象时的性能非常之差!
image.png

五、总结

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

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

六、结尾彩蛋

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

目录
相关文章
|
6月前
|
存储 Java 开发者
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
72 1
|
2月前
|
Java
死磕-java并发编程技术(二)
死磕-java并发编程技术(二)
|
2月前
|
存储 Java 调度
死磕-java并发编程技术(一)
死磕-java并发编程技术(一)
|
6月前
|
存储 缓存 Oracle
Java线程池,白话文vs八股文,原来是这么回事!
一、线程池原理 1、白话文篇 1.1、正式员工(corePoolSize) 正式员工:这些是公司最稳定和最可靠的长期员工,他们一直在工作,不会被解雇或者辞职。他们负责处理公司的核心业务,比如生产、销售、财务等。在Java线程池中,正式员工对应于核心线程(corePoolSize),这些线程会一直存在于线程池中。他们负责执行线程池中的任务,如果没有任务,他们会等待新的任务到来。 1.2、所有员工(maximumPoolSize) 所有员工:这些是公司所有的员工,包括正式员工和外包员工。他们共同组成了公司的团队,协作完成公司的各种业务。在Java线程池中,所有员工对应于所有线程(maxim
|
6月前
|
安全 Java 编译器
《Java核心卷1》慢慢啃!读第3,4章 | 第12版
第三章 Java的基本程序设计结构 1、变量与运算
50 0
|
缓存 安全 Java
Java并发编程必知必会面试连环炮
Java并发编程必知必会面试连环炮
150 0
|
缓存 Java 程序员
肝到头秃!百度强推并发编程笔记我爱了,原来这才叫并发
随着Java程序员的大幅增长,人们对Java程序员的要求也是越来越严苛。从现在Java岗的招聘需求来看,并发编程已经是我们Java程序员避不开的坎了! 编写正确的程序并不容易,而编写正确的并发程序就更难了。与顺序执行的程序相比,并发程序中显然更容易出现错误。而且并发性错误通常并不会以某种确定的方式显现出来。
|
SQL 缓存 Java
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
|
存储 监控 安全
阿里三面:让你怀疑人生的Java锁机制夺命连环21问
锁机制无处不在,锁机制是实现线程同步的基础,锁机制并不是Java锁独有的,其他各种计算机语言中也有着锁机制相关的实现,数据库中也有锁的相关内容,
157 0
深夜!小胖问我,什么是自旋锁?怎么使用?适用场景是啥?
深夜!小胖问我,什么是自旋锁?怎么使用?适用场景是啥?
深夜!小胖问我,什么是自旋锁?怎么使用?适用场景是啥?