首页 > 其他分享 >CopyOnWriteArrayList 是如何保证线程安全的?

CopyOnWriteArrayList 是如何保证线程安全的?

时间:2022-11-23 22:44:57浏览次数:69  
标签:index elements Object CopyOnWriteArrayList 保证 数组 线程 public

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。

前言

大家好,我是小彭。

在上一篇文章里,我们聊到了ArrayList 的线程安全问题,其中提到了 CopyOnWriteArrayList 的解决方法。那么 CopyOnWriteArrayList 是如何解决线程安全问题的,背后的设计思想是什么,今天我们就围绕这些问题展开。

本文源码基于 Java 8 CopyOnWriteArrayList。


小彭的 Android 交流群 02 群已经建立啦,扫描文末二维码进入~


思维导图:


1. 回顾 ArrayList

ArrayList 是基于数组实现的动态数据,是线程不安全的。例如,我们在遍历 ArrayList 的时候,如果其他线程并发修改数组(当然也不一定是被其他线程修改),在迭代器中就会触发 fail-fast 机制,抛出 ConcurrentModificationException 异常。

示例程序

List<String> list = new ArrayList();
list.add("xiao");
list.add("peng");
list.add(".");

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    // 可能抛出 ConcurrentModificationException 异常
    iterator.next();
}

要实现线程安全有 3 种方式:

  • 方法 1 - 使用 Vector 容器: Vector 是线程安全版本的数组容器,它会在所有方法上增加 synchronized 关键字(过时,了解即可);
  • 方法 2 - 使用 Collections.synchronizedList 包装类
  • 方法 3 - 使用 CopyOnWriteArrayList 容器

Collections.synchronizedList 包装类的原理很简单,就是使用 synchronized 加锁,源码摘要如下:

Collections.java

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

// 使用 synchronized 实现线程安全
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    final List<E> list;

    public boolean equals(Object o) {
        if (this == o) return true;
        synchronized (mutex) {return list.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return list.hashCode();}
    }

    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);}
    }
		...
}

如果我们将 ArrayList 替换为 CopyOnWriteArrayList,即使其他线程并发修改数组,也不会抛出 ConcurrentModificationException 异常,这是为什么呢?


2. CopyOnWriteArrayList 的特点

CopyOnWriteArrayList 和 ArrayList 都是基于数组的动态数组,封装了操作数组时的搬运和扩容等逻辑。除此之外,CopyOnWriteArrayList 还是用了基于加锁的 “读写分离” 和 “写时复制” 的方案解决线程安全问题:

  • 思想 1 - 读写分离(Read/Write Splitting): 将对资源的读取和写入操作分离,使得读取和写入没有依赖,在 “读多写少” 的场景中能有效减少资源竞争;
  • 思想 2 - 写时复制(CopyOnWrite,COW): 在写入数据时,不直接在原数据上修改,而是复制一份新数据后写入到新数据,最后再替换到原数据的引用上。这个特性各有优缺点:
    • 优点 1 - 延迟处理: 在没有写入操作时不会复制 / 分配资源,能够避免瞬时的资源消耗。例如操作系统的 fork 操作也是一种写时复制的思想;
    • 优点 2 - 降低锁颗粒度: 在写的过程中,读操作不会被影响,读操作也不需要加锁,锁的颗粒度从整个列表降低为写操作;
    • 缺点 1 - 弱数据一致性: 在读的过程中,如果数据被其他线程修改,是无法实时感知到最新的数据变化的;
    • 缺点 2 - 有内存压力: 在写操作中需要复制原数组,在复制的过程中内存会同时存在两个数组对象(只是引用,数组元素的对象还是只有一份),会带来内存占用和垃圾回收的压力。如果是 “写多读少” 的场景,就不适合。

所以,使用 CopyOnWriteArrayList 的场景一定要保证是 “读多写少” 且数据量不大的场景,而且在写入数据的时候,要做到批量操作。否则每个写入操作都会触发一次复制,想想就可怕。举 2 个例子:

  • 例如批量写入一组数据,要使用 addAll 方法 批量写入;
  • 例如在做排序时,要先输出为 ArrayList,在 ArrayList 上完成排序后再写回 CopyOnWriteArrayList。

3. CopyOnWriteArrayList 源码分析

这一节,我们来分析 CopyOnWriteArrayList 中主要流程的源码。

3.1 CopyOnWriteArrayList 的属性

ArrayList 的属性很好理解,底层是一个 Object 数组,我要举手提问

标签:index,elements,Object,CopyOnWriteArrayList,保证,数组,线程,public
From: https://www.cnblogs.com/pengxurui/p/16920416.html

相关文章

  • 关于配置执行定时任务和异步任务的线程池配置类
    packagecom.liftsail.rsademo.utils;importlombok.extern.slf4j.Slf4j;importorg.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;importorg.springf......
  • 手动创建线程池+自定义拒绝策略
    try{ThreadFactorytycThreadFactory=newThreadFactoryBuilder().setNamePrefix("tyc-call-inf").build();//拒绝策略,超过线程数+......
  • Python爬虫如何实现多线程异步
    如果自己的电脑配置高操作系统可以多任务运行的,应该首先要考虑单核CPU是怎么执行多任务的,操作系统会让各个任务交替执行。例如:任务1执行0.02秒,切换到任务2,任务2执行0.02秒,再......
  • python编程(改进的线程同步方式)
      在实际代码开发中,gui的代码并不好写。因为不管是mvc、还是mvp都有一定的局限性。那么,这个时候,我就在想,是不是可以用mvp+reactor的方法进行gui的改进操作呢?首先app编写......
  • 多线程的那点儿事(之大结局)
      多线程一直是我比较喜欢的话题,当然也是很多朋友比较害怕的话题。喜欢它,因为它确实可以提高pc的使用效率;讨厌它,因为如果对它处理不好,反而会导致更大的麻烦。这里断断续......
  • 多线程的那点儿事(之多核编程)
      多核编程并不是最近才兴起的新鲜事物。早在intel发布双核cpu之前,多核编程已经在业内存在了,只不过那时候是多处理器编程而已。为了实现多核编程,人们开发实现了几种多......
  • 搜索引擎的那些事(多线程web遍历)
       上面一篇博客当中,我们可以利用单一的线程完成网页的下载。今天,我们打算在此基础上完成多线程的访问和加载操作。使用多线程,倒不是因为这项技术有多牛,主要是因为我们......
  • 嵌入式操作系统内核原理和开发(多线程轮转)
       之前我们也谈到了线程创建,基本上简单的系统就可以跑起来了,但是还没有到多线程运行的地步。所以,我们下面试图所要做的工作就是创建更多的线程,让更多的线程运行起来。......
  • Linux的多线程
     线程的概念和多进程相比,多线程是一种比较节省资源的多任务操作方式。启动一个新的进程必须分配给它独立的​​地址空间​​​,每个进程都有自己的​​堆栈段​​​和​​数......
  • Linux线程同步介绍和示例
     线程同步的概念    线程同步?怎么同步?一起运行?一起停止?我当年听说线程同步这个词的时候,也是一头雾水。    在人们的日常生活中,所说的锁大概有两种:一种是不允......