1. 为什么实时配置管理需要CopyOnWriteArrayList
做后台开发的朋友,肯定都遇到过配置管理的需求。比如,你的系统里有一个开关,控制着某个功能的开启和关闭;或者有一批白名单用户,可以访问特定的接口。这些配置,在系统运行过程中,很可能需要动态调整,也就是我们常说的“热更新”。你总不想为了改个配置,就重启整个服务吧?那体验太差了。
在单机时代,这问题好解决,一个 HashMap 或者 Properties 对象,加载到内存里,改的时候加个锁,防止多线程读写冲突就行了。但到了高并发、分布式的微服务时代,事情就变得复杂了。成百上千个线程可能在同一时刻读取配置,而管理员可能每隔几分钟、几小时才更新一次配置。这时候,如果你还用传统的 synchronized 或者 ReentrantLock 把整个配置对象锁住,读操作就会陷入无谓的等待,系统吞吐量会急剧下降。我早年就踩过这个坑,用 Collections.synchronizedList 包装了一个配置列表,上线后性能监控一看,CPU没怎么用,但QPS就是上不去,瓶颈全在锁竞争上了。
这时候,CopyOnWriteArrayList 就该登场了。它的设计哲学非常巧妙,叫做“写时复制”。简单来说,就是“读写分离”的极致体现。所有的读操作,完全不加锁,直接访问当前数组,速度快得飞起。而写操作(增、删、改)则通过“复制-修改-替换”三步曲来保证线程安全。当你要更新配置时,它会悄悄地把整个配置数组复制一份,在这个副本上完成修改,然后再原子性地把内部引用指向这个新数组。对于正在读取的线程来说,它们毫无感知,依然流畅地读着旧数组;对于新来的读请求,它们自然就读到了新配置。
这种“弱一致性”模型,恰恰是很多配置管理场景所追求的。我们并不要求某个线程在更新配置的“那一瞬间”,全世界所有其他线程都必须立刻看到新值。我们允许有一个极其短暂的时间窗口,部分请求读到的是旧配置。只要这个窗口足够短(通常就是一次内存复制的耗时),对于业务来说就是可以接受的。比如,你把某个功能的超时时间从30秒改成了60秒,可能就有那么几个正在处理的请求,仍然按30秒的超时逻辑在执行,这通常不会造成灾难性后果。用一点点“最终一致性”的代价,换来读性能的巨幅提升,这笔买卖在“读多写少”的场景里,简直太划算了。
2. 核心机制:写时复制如何保障配置热更新
光说概念可能有点虚,我们直接扒开 CopyOnWriteArrayList 的源码,看看它到底是怎么工作的。理解了原理,用起来心里才有底。
2.1 数据结构与写操作全流程
CopyOnWriteArrayList 内部就维护了一个核心的东西:一个用 volatile 关键字修饰的数组引用,叫 array。这个 volatile 是关键,它保证了多线程下的内存可见性。一旦这个引用被修改,其他所有线程都能立即看到。
假设我们用它来存一个功能开关列表:
private volatile CopyOnWriteArrayList<String> featureFlags = new CopyOnWriteArrayList<>();
featureFlags.add("new_user_gift"); // 初始状态
现在,管理员要通过管理后台新增一个开关 “ai_recommend”。这时,add 方法内部上演了这样一幕:
- 加锁:首先,它会获取一个唯一的
ReentrantLock锁。注意,这个锁只针对写操作。这意味着同一时间,只能有一个线程在执行修改(比如add、set、remove),但无数个读线程可以同时畅行无阻。 - 复制:上锁后,它把当前的
array(假设是["new_user_gift"])完整地拷贝到一个新的数组中。新数组的长度是旧数组长度+1。 - 修改:在新数组的末尾,放入新元素
"ai_recommend"。此时,新数组是["new_user_gift", "ai_recommend"],旧数组依然原封不动。 - 切换:最关键的一步,将
volatile的array引用指向这个刚创建好的新数组。这个赋值操作是原子的,并且得益于volatile,新引用立即可见。 - 解锁:释放锁,写操作完成。
整个过程,旧数组就像一张被定格的快照,没有任何改动。所有在写操作开始前,或者在整个复制替换过程中正在进行读操作的线程,访问的都是这张不变的快照。这就是读操作无需加锁也能线程安全的根本原因。
2.2 读操作的无锁狂欢与弱一致性体现
读操作就简单得令人发指了。比如我们要检查某个功能是否开启:
public boolean isFeatureOn(String featureName) {
// 直接访问当前 array 引用指向的数组,没有任何同步开销
return featureFlags.con

2万+

被折叠的 条评论
为什么被折叠?



