一文搞懂Java的CopyOnWriteArrayList:从原理到实战,连面试官都直呼内行!

268 阅读4分钟

一文搞懂Java的CopyOnWriteArrayList:从原理到实战,连面试官都直呼内行!


引言:当ArrayList有了“分身术”,多线程再也不怕翻车了!

在Java的并发编程江湖中,ArrayList因为“线程不安全”的标签被各路高手嫌弃,而Vector又因“全副武装的锁”导致性能拉胯。这时,一位名叫 CopyOnWriteArrayList(COW) 的侠客横空出世,凭借“写时复制”的独门绝技,在“读多写少”的江湖场景中一战封神!今天我们就来扒一扒这位侠客的底裤(划掉)……底层原理!


一、CopyOnWriteArrayList是什么?

1.1 一句话定义

COW是Java并发包(JUC)中的线程安全List,通过“写时复制”策略实现读写分离,读操作无锁,写操作加锁复制新数组,堪称“读多写少”场景的救星。

1.2 核心特点

  • 读操作不锁:随便读,反正你读的是原数组的“快照”;
  • 写操作复制:每次修改都像搬家一样,先复制整个房子(数组),在新家装修(修改),最后把门牌号(引用)换掉;
  • 迭代器安全:遍历时用的是“历史快照”,不怕中途被修改。

二、用法与案例:黑名单系统的正确打开方式

2.1 基础操作

// 初始化
CopyOnWriteArrayList<String> blacklist = new CopyOnWriteArrayList<>();

// 添加元素(线程安全)
blacklist.add("hacker1");

// 读取元素(无锁,高性能)
System.out.println(blacklist.get(0)); // 输出:hacker1

// 遍历(不会抛ConcurrentModificationException)
Iterator<String> it = blacklist.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
    // it.remove(); // 报错!迭代器不支持修改操作
}

2.2 实战案例:黑名单系统

假设你运营一个社交平台,每天有1万次用户搜索请求,但黑名单每晚更新一次。用COW实现:

// 初始化黑名单
CopyOnWriteArrayList<String> blacklist = loadBlacklistFromDB();

// 用户搜索时检查(高频读操作,无锁)
public boolean isBlocked(String keyword) {
    return blacklist.contains(keyword);
}

// 定时更新黑名单(低频写操作)
public void updateBlacklist(List<String> newList) {
    blacklist.clear();
    blacklist.addAll(newList);
}

优势:白天用户疯狂搜索时,读操作丝滑无阻;夜间更新时,短暂的写锁对用户体验无感。


三、原理揭秘:写时复制的“影分身术”

3.1 核心机制

  • 写操作流程(以add为例):
    1. 加锁:用ReentrantLock保证同一时间只有一个线程能写;
    2. 复制数组:把原数组完整拷贝到新数组(长度+1);
    3. 修改新数组:在新数组末尾添加元素;
    4. 替换引用:将volatile修饰的数组指针指向新数组。
// add方法源码(简化版)
public boolean add(E e) {
    lock.lock();
    try {
        Object[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1);
        newArray[newArray.length - 1] = e;
        setArray(newArray); // volatile写保证可见性
    } finally {
        lock.unlock();
    }
}
  • 读操作:直接访问volatile数组,无需锁,性能炸裂。

3.2 迭代器的秘密

迭代器内部保存的是创建时的数组快照,即使其他线程修改了数据,迭代器看到的依然是“历史版本”,因此不会抛ConcurrentModificationException


四、对比:COW vs Vector vs SynchronizedList

特性CopyOnWriteArrayListVectorSynchronizedList
锁粒度写操作加锁,读无锁全方法synchronized同步块(部分方法优化)
并发性能读性能极高,写性能差读写均差读性能一般,写性能差
内存占用写操作内存翻倍
数据一致性最终一致性强一致性强一致性
适用场景读多写少(如黑名单、缓存)已过时,不推荐写少且需强一致性

总结:COW是“读霸”,Vector是“古董”,SynchronizedList是“折中派”。


五、避坑指南:COW的“七寸”在哪里?

  1. 内存炸弹:频繁写操作会导致大量数组拷贝,引发GC风暴。切忌在数据量大的高频写场景使用。
  2. 数据延迟:读到的可能是旧数据,不适合实时交易系统。比如“秒杀库存”场景会翻车。
  3. 不支持排序:直接调用Collections.sort()会抛异常!正确做法是先转成普通List排序,再替换。
    List<String> temp = new ArrayList<>(copyOnWriteList);
    Collections.sort(temp);
    copyOnWriteList.clear();
    copyOnWriteList.addAll(temp);
    

六、最佳实践:什么时候该请COW出山?

  • 适用场景

    • 读操作占比超过90%(如配置项、缓存);
    • 数据量较小(避免复制大数组);
    • 允许短暂数据不一致(最终一致性)。
  • 不适用场景

    • 高频写操作(如股票实时报价);
    • 大数据量(内存扛不住);
    • 强一致性要求(如银行余额)。

七、面试考点:COW的“灵魂拷问”

7.1 高频问题

  1. COW的读写流程是怎样的?
    (答:写时复制+锁,读无锁,引用volatile数组)

  2. 为什么迭代器遍历时不会抛ConcurrentModificationException?
    (答:迭代器用的是创建时的数组快照)

  3. COW和读写锁的区别?
    (答:COW读完全无锁,读写锁读仍需竞争锁)

7.2 陷阱题

  • “COW的size()方法实时准确吗?”
    (答:不!因为size()返回的是当前数组长度,可能已被其他线程修改)

八、总结:COW的“武功秘籍”

优点:读操作快到飞起,迭代器安全,代码简洁;
缺点:写操作慢如蜗牛,内存消耗大,数据延迟;
精髓读多写少用COW,写多读少快绕道!


彩蛋:COW的哲学启示

人生何尝不是一场“写时复制”?—— 想要改变现状,就得先拷贝一份自我,在新副本上努力升级,最后让世界看到全新的你!(当然,锁还是要加的,毕竟同一时间只能专心做一件事……)