Kotlin中不可变列表是否需要加锁?并发操作该列表需同步吗?
Kotlin 多线程下的不可变列表并发问题解答
基础结论
- 没有任何同步约束的前提下,
One()和Two()完全可以被不同线程同时调用、并行执行,不存在默认的串行执行限制。 - 上述代码在多线程场景下存在明确的并发风险,是否需要加锁保护要结合业务需求判断,不能笼统认为“用了不可变列表就不需要任何同步”。
核心认知拆解
这里很容易踩中一个常见的认知误区:把两个完全不同的线程安全问题混为一谈。
listOf()返回的是不可变集合对象:这类对象没有任何修改内部元素、调整容量的公开方法,只要这个对象的引用对所有线程完整可见,所有线程对它的遍历、读取操作都是完全安全的,不会出现ConcurrentModificationException,也不会因为多线程并发读就出现内部状态错乱。- 但代码里用来持有列表的变量是
var list——这是一个可变的引用,本身没有任何并发访问保障,这才是整个代码的风险来源。反过来如果list是val修饰的固定不可变引用,搭配不可变的集合对象,整个链路没有可变状态,天然就是线程安全的,不需要任何同步手段。
现有代码的具体并发风险
当两个函数被不同线程同时调度时,会出现两类典型问题:
- 可见性丢失:没有同步机制的前提下,
Two()对list的重新赋值可能一直停留在执行线程的CPU工作内存,不会及时刷回主内存。执行One()的线程可能永远感知不到这个赋值变化,一直拿着最早的旧列表执行业务逻辑,和预期的语义完全不符。 - 指令重排风险:对象的创建流程(分配内存、执行构造方法、将引用赋值给变量)在没有同步约束时,顺序可能被JVM调整打乱。其他线程可能拿到一个已经完成赋值的引用,但它指向的是一个还没走完构造流程的半初始化列表,这时候执行遍历就可能触发空指针、数组越界等难以排查的异常。
到底要不要加锁?
完全不需要为了“不可变列表”这个标签盲目加锁,具体方案要匹配业务需求:
- 如果业务要求
One()从进入函数到遍历完成的全流程,必须使用同一份固定版本的列表、不中途被替换引用,那就需要加锁同时保护两个函数对list的读写操作。 - 如果业务只要求每次读取
list时能拿到最新的、完整初始化好的列表版本,遍历过程中就算引用被替换也不影响当前遍历的逻辑(因为不可变列表本身永远不会被修改),那完全不需要加重型锁,只要给可变的引用补上并发保障即可:- 给
list加上@Volatile注解,保证赋值操作的可见性、禁止对象创建流程的指令重排; - 或者直接用
AtomicReference持有列表引用,所有对引用的读、写操作都通过原子引用完成。
这种场景下两个函数依然可以完全并行运行:执行One()的线程拿到引用后,遍历的是那个完整、不可变的列表对象;执行Two()的线程只是往原子引用里写入一个全新的不可变列表,两个操作不会对同一个内存对象产生修改,不存在竞态条件。
- 给
内容的提问来源于stack exchange,提问作者Aadhavan S
相关产品推荐
相关产品推荐

