Java并发List实战:CopyOnWriteArrayList原理与ArrayList常见面试题

  • 博客主页:天天困啊
  • 系列专栏:面试题
  • 关注博主,后期持续更新系列文章
  • 如果有错误感谢请大家批评指出,及时修改
  • 感谢大家点赞👍收藏⭐评论✍

首先我希望大家想一个问题,也是常考的面试题之一

ArrayList线程真的安全吗?

我先给大家提供一下我写的模拟多线程并发环境下的ArrayList代码

java 复制代码
/**
 * @author 天天困
 * @date 2025/11/4
 */
public class Test01 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 50; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

我们看一下控制台,没错报错了,所以我们通过代码就证实了它并不是安全的

所以我们第一道常考的面试题,ArrayList线程安全吗的答案就是:

它不是线程安全的

那由此我们可以延伸出第二道面试题,也就是那我们知道它不是安全的了,有没有什么方法让他变为安全的?

把ArrayList变成线程安全的方法有哪些?

一般线程安全这种知识是属于JUC中的,那在JUC中我们最熟知的一个知识就是锁,那最常见的锁就是synchronized

第一种

java 复制代码
// 使用Collections类的synchronizedList
List<String> list = new ArrayList<>();
List<String> list1 = Collections.synchronizedList(list);

第二种

它是属于JUC中的知识也是比较常用的属于并发包下的

java 复制代码
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

第三种

ArrayList它是在util包下的,那我们可以思考以下在以前是否有没有学过线程是安全的集合还是在util包下的

上图是Vector中一些操作的源码,我们可以从源码就看到这些操作他都使用了synchronized,这些操作就证明了是线程安全的

在Vector的源码中,官方也说明了

那我们现在知道了它线程是不安全的,我们是不是应该思考一下为什么它的线程不是安全的?这也就是我们的下一道常考的面试题

为什么ArrayList线程不是安全的?

我们在第一道面试题中给大家写过一个模拟多线程安全的代码,还有控制台的报错信息,那我们就会发现其实在高并发添加数据的情况下会触发三个问题

  • 有的值可能是null,他的数据并不是对等的。因为ArrayList 的方法没有使用 synchronized 关键字修饰
  • 索引越界问题
  • size计数器问题不准确

我们还是有疑问就去通过源码来解析,这是每一个程序员必会的知识!

ensureCapacityInternal这个代码的具体实现我们可以先忽略不计,我们先看后边的。它主要作用是判断将新元素添加到列表后,然后看列表的数组大小是否满足的,满足就进行size+1操作,我们都知道ArrayList有扩容机制,如果size+1的之后的长度大于了这个数组的长度,我们就要进行扩容操作

下面我给大家讲解一下为什么会出现上述的那三个问题

  • 有的值可能是null:这个原因是,我们在高并发的情况下,我们并没有做加锁的一些机制,会造成两个线程同时抢占一个索引位置。比如我们线程1发现size的数量够,并没有进行+1操作,这时线程2也进来了,线程1还没有完成+1的操作,所以线程2进来的时候和线程1看到的索引位置和数组长度是相同的,所以并没有触发扩容机制,这个时候线程1完成了+1操作,紧接着线程2也完成了+1操作,这个时候他俩都进行了size++,那就会导致下一个size的位置为null了
  • 索引越界:也跟第一条解释类似,我们并没有触发扩容机制,导致数组大小没有变化,当前长度已经超过数组的长度了,就会触发索引越界问题
  • size计数器问题不准确:因为size++并没有做一下原子性的操作,所以可能导致线程1和线程2同时进行size++,会把一个值覆盖掉,所以可能导致计数有问题

我们在上一道面试题的解析中,多次提到了扩容机制。没错我们下一道常考的面试题就是ArrayList的扩容机制

ArrayList的扩容

ArrayList的底层是基于数组实现的,所以他默认的长度是10。对比数组它提供了动态扩容能力,如果我们要添加第11个数据的时候,就超过了默认长度,这个时候我们就会触发扩容

  • 它扩容会扩大为原容量的1.5倍(通过位运算,上图中有源码)
  • 根据计算创建一个新数组
  • 将原数组中的元素复制到新数组中

在第二道面试题中我给大家讲解了线程安全的方法有哪些,其中我们讲到了一个CopyOnWriteArrayList,最后一道面试题就是

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

从源码中我们可以看到他底层其实也是一个数组,使用了volatile关键字修饰,保证了不同线程对这个数组操作时的可见性

源码中我们可以看到写的操作他加了锁,我们看到他将老数组拷贝了一份然后长度+1得到新数组newElements,然后他把新加入的元素都放到了新数组的最后一位,在把新数组的地址值替换掉老数组的就可以得到了最新的数据

CopyOnWriteArrayList的核心是写时复制(Copy-On-Write,COW)机制:读操作直接访问底层数组,无需加锁;写操作复制一份新的数组,在新数组上执行修改,完成后替换原数组(通过锁保证原子性)

相关推荐
代码or搬砖2 小时前
Docker 部署 Java 项目实践
java·docker·容器
又是忙碌的一天2 小时前
抽象类和接口
java·开发语言
亮剑20182 小时前
第2节:程序逻辑与控制流——让程序“思考”
开发语言·c++·人工智能
lly2024062 小时前
Go 语言接口
开发语言
霜绛2 小时前
C#知识补充(二)——命名空间、泛型、委托和事件
开发语言·学习·unity·c#
August_._2 小时前
【MySQL】SQL语法详细总结
java·数据库·后端·sql·mysql·oracle
Dxxyyyy2 小时前
零基础学JAVA--Day26(枚举类)
java·开发语言
好望角雾眠3 小时前
第四阶段C#通讯开发-6:Socket之UDP
开发语言·笔记·学习·udp·c#
黑屋里的马3 小时前
java的设计模式之桥接模式(Bridge)
java·算法·桥接模式