ArrayList线程不安全怎么办?

有三种解决方法:

  • 使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字

    • 就和 HashMap 和 HashTable 的关系一样
  • 使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下:

    List<Integer> list = Collections.synchronizedList(new ArrayList<>());
    • 其实 HashMap 也可以用这招:

      Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    • 这个看上去有点东西,其实也是给每个方法加上一个 synchronized,不过不是直接加在方法上,而是加在方法内部,只有当线程获取到 mutex 这个对象的锁,才能进入代码块:

      public E get(int index) {
      synchronized (mutex) {
      return list.get(index);
      }
      }
  • 使用 JUC 包下提供的 CopyOnWriteArrayList

    • 其实 ConcurrentHashMap 也是 JUC 包下的

这里具体讨论一下 CopyOnWriteArrayList 这个类,它采用了“写时复制”的技术,也就是说,每当要往这个 list 中添加元素时,并不是直接就添加了,而是会先复制一份 list,然后在这个复制中添加元素,最后再修改指针的指向,看看 add 的源码:

public boolean add(E e) {
synchronized (lock) {
//得到当前的数组
Object[] es = getArray();
int len = es.length;
//复制一份并扩容
es = Arrays.copyOf(es, len + 1);
//把新元素添加进去
es[len] = e;
//修改指针的指向
setArray(es);
return true;
}
}

有人可能会疑惑,这有什么意义,这不也加了 synchronized 吗,而且还要复制数组,这**不是比 Vector 还要烂吗?

确实是这样的,在写操作比较多的场景下,CopyOnWriteArrayList 确实比 Vector 还要慢,但它有两个优势:

  • 虽然写操作烂了,但读操作快了很多,因为在 vector 中,读操作也是需要锁的,而在这里,读操作就不需要锁了,get 方法比较短可能不便于理解,我们看看 indexOf 这个方法:

    public int indexOf(Object o) {
    Object[] es = getArray();
    return indexOfRange(o, es, 0, es.length);
    }
    private static int indexOfRange(Object o, Object[] es, int from, int to) {
    if (o == null) {
    for (int i = from; i < to; i++)
    if (es[i] == null)
    return i;
    } else {
    //****here****
    for (int i = from; i < to; i++)
    if (o.equals(es[i]))
    return i;
    }
    return -1;
    }
    • 可以发现,这个方法先把当前数组 array 交给了 es 这个变量,后续的所有操作都是基于 es 进行的(此时 array 和 es 都指向内存中的同一份数组 a1)
    • 由于所有写操作都是在 a1 的拷贝上进行的(我们把内存中的这份拷贝称为 a2),因此不会影响到那些正在 a1 上进行的读操作,并且就算写操作执行完毕了,array 指向了 a2,也不会影响到 es 这个数组,因为 es 指向的还是 a1
    • 试想,如果 vector 的读操作不加锁会出现什么情况?由于 vector 中所有的读写操作都是基于同一个数组的,因此虽然读操作一开始拿到的数组是没问题的,但在后续遍历的过程中(比如上面代码标注了 here 的地方),很可能出现其他线程对数组进行了修改,夸张点说,如果有个线程把数组给清空了,那么读操作就肯定会报错了,而对于 CopyOnWriteArrayList 来说,就算有清空的操作,那也是在 a2 上进行的,而读操作还是在 a1 上进行,不会有任何影响
  • 在 forEach 遍历一个 vector 时,是不允许对 vector 进行修改的,会报出 ConcurrentModificationException 这个异常,理由很简单,因为只有一份数组,要是遍历到一半有其它线程把数组清空了不就出问题了吗,因此 java 干脆就直接禁止这种遍历时修改数组的行为了,但对于 CopyOnWriteArrayList 来说,它的遍历是一直在 a1 上进行的,其它写线程只能修改到 a2,这对 a1 是没有任何影响的,我们看一段代码来验证一下:

    public class Test {
    public static void main(String[] args) {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 1000; i++) {
    list.add(i);
    }
    //遍历时把数组清空
    for (Integer i : list) {
    System.out.println(i);
    list.clear();
    }
    }
    }
    • 结果是没有报错,并且完整输出了 0~999 所有的数字,可见这里遍历的就是最开始的那个数组 a1,期间哪怕有再多的写操作也不会影响到 a1,因为所有的写操作都是在 a2 a3 a4 上进行的

综上所述,CopyOnWriteArrayList 的优点就是,读操作很快,不需要锁,并且支持遍历,遍历过程中就算数组被修改也不会报错,在读多写少的场景下是很优秀的

但它的缺点也很明显,主要有两点:

  • 首先,写操作的内存消耗非常大,每次修改数组都会进行一次拷贝,如果数组比较大或者修改次数比较多,很快就会消耗掉大量内存,触发 GC,因此在写多的场景下一定要慎用这个类
  • 其次,虽然读操作和遍历不需要上锁,也允许遍历时数组被修改,但这些操作都是基于旧数组 a1 的,在它们执行 Object[] es = getArray() 这条语句的一瞬间就决定了它们所要操作的数组,因此它们是没有办法感知到最新的那些数据的,就算途中新增了一个很重要的数据,这个数据也是在 a2 中,遍历 a1 是无法得到这个数据的

ArrayList线程不安全怎么办?(CopyOnWriteArrayList详解)的更多相关文章

  1. java线程池的使用与详解

    java线程池的使用与详解 [转载]本文转载自两篇博文:  1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html   ...

  2. 剑指Offer——线程同步volatile与synchronized详解

    (转)Java面试--线程同步volatile与synchronized详解 0. 前言 面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现 ...

  3. Java线程创建形式 Thread构造详解 多线程中篇(五)

    Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制. 创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象. 构造方法列表 ...

  4. python3 线程 threading.Thread GIL性能详解(2.3)

    python3 线程 threading 最基础的线程的使用 import threading, time value = 0 lock = threading.Lock() def change(n ...

  5. Jmeter系列(10)- 阶梯加压线程组Stepping Thread Group详解

    如果你想从头学习Jmeter,可以看看这个系列的文章哦 https://www.cnblogs.com/poloyy/category/1746599.html 前言 Stepping Thread ...

  6. 从线程池到synchronized关键字详解

    线程池 BlockingQueue synchronized volatile 前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击.很多熟悉而又陌生的知识 ...

  7. CopyOnWriteArrayList详解

    可以提前读这篇文章:多读少写的场景 如何提高性能 写入时复制(CopyOnWrite)思想 写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略.其核心思想是,如果 ...

  8. Java线程池(ThreadPool)详解

    线程五个状态(生命周期): 线程运行时间 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间.    如果:T1 + T3 远大于 T2,则可以 ...

  9. 线程安全之CAS机制详解(分析详细,通俗易懂)

    背景介绍:假设现在有一个线程共享的变量c=0,让两个线程分别对c进行c++操作100次,那么我们最后得到的结果是200吗? 1.在线程不安全的方式下:结果可能小于200,比如当前线程A取得c的值为3, ...

随机推荐

  1. Python开发篇——构建虚拟Python开发环境(Conda+Poetry)

    前言 之前虽略有提及Python,但是没有实际地写点料.惭愧,惭愧,所以这次先起个头,讲讲如何构建虚拟Python开发环境.相信之前看过我博客的人可能会想:博主不会又要聊聊Docker吧?放心,不会. ...

  2. 利用 PGO 提升 .NET 程序性能

    引子 .NET 6 开始初步引入 PGO.PGO 即 Profile Guided Optimization,通过收集运行时信息来指导 JIT 如何优化代码,相比以前没有 PGO 时可以做更多以前难以 ...

  3. [HNOI2008]GT考试 题解

    这题比较难搞.考虑设计状态:\(f_{i,j}\) 表示当前考虑到 \(X_i\) 位,且 \(X\) 的后 \(j\) 位刚好与 \(A\) 列匹配时的方案数.最终答案为 \(\sum_{i=0}^ ...

  4. C++模板封装Win32 API 动态调用

    起因 花两周通读了一遍<C++ Primer>,积攒的疑惑一扫而光. 前因 利用C++11可变模板,封装调用dll导出函数 本以为已经很好用了,最近抽时间巩固下知识体系,发现自己道行不够! ...

  5. Drupal 远程代码执行漏洞(CVE-2018-7602)

    影响版本 Drupal Core 存在一个远程代码执行漏洞,影响 7.x 和 8.x 版本 如下图所示,执行以下命令即可复现该漏洞.示例命令为id,如图红框中显示,可以执行该命令. 检测 python ...

  6. 后台程序编译过程报错PCC-F-02104, Unable to connect to Oracle

    偶然重新编译了一下后台程序,发现编译过程报错无法连接数据库.但通过sqlplus登录数据库是正常的.后台程序改动中也做了详细的分析,没有改动相关数据库的参数和配置. 最后通过浏览器查看了很多相关问题的 ...

  7. Apache解析漏洞

    多解析特性 在Apache1.x,2.x中Apache 解析文件的规则是从右到左开始判断解析,如果后缀名为不可识别文件解析,就再往左判断.因此对于apache而言,一个test.php.qwea文件依 ...

  8. ElasticSearch进阶篇(一)--版本控制

    一.前言 ElasticSearch(以下简称ES)的数据写入支持高并发,高并发就会带来很普遍的数据一致性问题.常见的解决方法就是加锁.同样,ES为了保证高并发写的数据一致性问题,加入了类似于锁的实现 ...

  9. Install and Update autojump and oh-my-zsh Behind a Firewall

    Brief installation steps: Install git and zsh via yum; 'git clone' autojump and oh-my-zsh from an in ...

  10. 详细讲讲netty的pipiline!

    前言 提到 Netty 首当其冲被提起的肯定是支持它承受高并发的线程模型,说到线程模型就不得不提到 NioEventLoopGroup 这个线程池,接下来进入正题. 线程模型 首先来看一段 Netty ...