list并发与共享

1) 并发 + 共享的本质风险

  • 数据竞争 :多个线程同时读写同一 List,没有happens-before 关系 → 读到旧值/中间态。

  • 复合操作非原子 :if (!list.contains(x)) list.add(x) 在并发下会重复/丢失

  • fail-fast ≠ 线程安全 :ArrayList/LinkedList 的迭代器只是发现异常修改就抛 CME,不是并发保护。

只要"多个线程+同一可变 List 引用",你就需要:同步、不可变、或改用并发容器/消息化


2) 选型总览(什么时候用什么)

方案 读取 写入 一致性 代价 典型场景
Collections.synchronizedList + 外部同锁遍历 强一致 大锁、易阻塞 UI 少量共享,简单粗暴
ReentrantReadWriteLock 快(多读) 强一致 代码复杂 读远多于写
CopyOnWriteArrayList 读非常快(无锁快照) O(n) (写时复制) 读弱一致(快照) 写放大、双倍内存峰值 监听器/订阅者
快照读取:new ArrayList<>(list) / toList() 读快照 与快照一致 拷贝成本 一次性读、大列表 UI
持久化集合(Kotlin persistentList + AtomicReference) 中(结构共享) 强一致(版本化) 新对象分配 并发读多写少
并发容器替代(如 ConcurrentLinkedQueue) 弱一致 语义变化 队列/流式场景
消息化/所有权(Actor/Channel) 强一致(串行) 架构改动 Android 主线程/VM 归属

3) 基础规则(JMM 可见性与安全发布)

  • 可见性 :锁/volatile/final 构成的 happens-before 让别的线程看见最新内容。
  • 安全发布 :把可变 List 暴露前,最好完成构建后一次性发布(volatile 字段、通过锁发布、或不可变副本)。
  • 倾向不可变元素:即使集合安全,若元素对象自身可变仍会"穿透"一致性。

4) 常用做法与落地代码

A. 外部同步(强一致,简单粗暴)

scss 复制代码
List<Item> list = Collections.synchronizedList(new ArrayList<>());

// 修改或遍历都包同一把锁
synchronized (list) {
  // 写
  if (!list.contains(x)) list.add(x);
}

synchronized (list) {
  // 遍历(文档要求!)
  for (Item it : list) { /* ... */ }
}
  • ✅ 读写都一致;❌ 并发度低,不要在主线程长时间持锁(Android)。

B. 读多写少:ReadWriteLock

csharp 复制代码
class Repo {
  private final List<Item> list = new ArrayList<>();
  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  private final Lock r = lock.readLock(), w = lock.writeLock();

  List<Item> snapshot() { r.lock(); try { return new ArrayList<>(list); } finally { r.unlock(); } }
  void add(Item x) { w.lock(); try { list.add(x); } finally { w.unlock(); } }
}
  • ✅ 多读并行;❌ 代码复杂,写仍阻塞读。

C. 监听器/订阅者:CopyOnWriteArrayList

scss 复制代码
final CopyOnWriteArrayList<Listener> ls = new CopyOnWriteArrayList<>();
// 读:永不 CME、无锁快照
for (var l : ls) l.onEvent(e);
// 写:O(n) + 短暂双倍内存,频繁写不适用
ls.add(listener); ls.remove(listener);
  • ✅ 读极多写极少;❌ 列表大或写频繁会很贵。

D. 快照读取(UI 常用)

scss 复制代码
List<Item> snap; 
synchronized (list) { snap = new ArrayList<>(list); } // 或 AtomicReference.get()
for (Item it : snap) { /* UI 渲染,完全无锁 */ }
  • ✅ 渲染时零锁、无 CME;❌ 每次读有拷贝成本(可在后台线程完成)。

E. 持久化集合 + 原子交换(Kotlin 推荐)

kotlin 复制代码
import kotlinx.collections.immutable.*
import java.util.concurrent.atomic.AtomicReference

class Store {
  private val ref = AtomicReference<PersistentList<Item>>(persistentListOf())

  fun snapshot(): List<Item> = ref.get()               // 读:无锁
  fun add(x: Item) {
    while (true) {
      val cur = ref.get()
      val next = cur.add(x)                            // 返回新结构(共享大部分节点)
      if (ref.compareAndSet(cur, next)) break
    }
  }
}
  • ✅ 读无锁、版本化一致;❌ 修改会分配新对象(但结构共享,通常可接受)。

F. 不共享可变结构:消息化/所有权(Actor)

  • 思想 :把 List 的"所有权"限定在单线程 (UI/某个协程作用域),跨线程通过消息修改。
kotlin 复制代码
class Actor(scope: CoroutineScope) {
  private val mailbox = Channel<(MutableList<Item>) -> Unit>(Channel.UNLIMITED)
  private val state = mutableListOf<Item>()

  init { scope.launch(Dispatchers.Default) {
    for (msg in mailbox) msg(state) // 串行应用修改
  } }

  fun offerAdd(x: Item) = mailbox.trySend { it.add(x) }
  fun snapshot(): List<Item> = state.toList() // 若在 UI 线程暴露,直接 toList()
}
  • ✅ 不再共享可变结构,天然强一致;❌ 需要架构约束。

G. 并发容器替代(语义转移)

  • 队列/流水线:ConcurrentLinkedQueue / ArrayBlockingQueue(不要强行用 List)。
  • Key 集合:ConcurrentHashMap.newKeySet() 代替 "List + contains"。

5) 复合操作要么原子,要么分层

  • check-then-act 原子化
scss 复制代码
synchronized (list) {
  if (!list.contains(x)) list.add(x);
}
  • 批处理 :把多次改动合成一个提交,一次性加锁或通过 Actor 执行。
  • 遍历中删除:单线程用迭代器 remove() 或 removeIf;跨线程不要这么干,换"快照+重建"。

6) Android 落地建议(UI/VM/Repo 三段式)

ViewModel 持有状态(不可变暴露)

kotlin 复制代码
data class UiState(val items: List<Item>)
private val _state = MutableStateFlow(UiState(emptyList()))
val state: StateFlow<UiState> = _state

// 修改时用不可变副本 + Diff/计算在后台
fun add(x: Item) = viewModelScope.launch(Dispatchers.Default) {
  val next = _state.value.items.toMutableList().apply { add(x) }.toList()
  _state.update { it.copy(items = next) }
}

RecyclerView 更新 :用 ListAdapter / DiffUtil,不要 在适配器内部共享/修改可变 List;每次提交新 List 快照

避免在主线程持锁 :重活放 Dispatchers.Default/IO,UI 只消费 快照/不可变


7) 典型坑位速查

  • synchronizedList 只包住单次方法遍历必须手动同锁,否则仍可能 CME。
  • subList/Arrays.asList/unmodifiableList 都是视图:共享底层,跨引用修改易 CME 或数据"凭空改变"。
  • 把内部可变 List 原样返回 :外部一改你就乱;对外拷贝 + 只读包装
  • CopyOnWrite 用错地方:大列表或高频写会拖垮内存与 CPU。
  • "size+for(i)" 与并发写:结构变化期间按下标遍历会越界/遗漏;改"快照+forEach"。

8) 决策树(面试/落地小抄)

  1. 读多写少? → CopyOnWriteArrayList(监听器)或 快照读取
  2. 强一致? → synchronizedList + 同锁遍历,或 ReadWriteLock
  3. 需要高吞吐并行? → 换并发容器(队列/映射),别勉强用 List。
  4. 架构允许?Actor/消息化,不共享可变结构。
  5. UI 列表? → ViewModel 暴露不可变快照,ListAdapter+DiffUtil 增量刷新。

结论

默认策略不共享可变 List ,对外只给快照/不可变 ;需要共享时,要么加锁 ,要么用合适的并发容器 ;监听器用 COW ;实时场景优先消息化/所有权。这样既稳又好维护。

相关推荐
Lee川13 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川16 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i18 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有18 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有18 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫19 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫20 小时前
Handler基本概念
面试
Wect20 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼21 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼21 小时前
Next.js 企业级落地
前端·javascript·面试