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 ;实时场景优先消息化/所有权。这样既稳又好维护。

相关推荐
ShooterJ4 小时前
Mysql小表驱动大表优化原理
数据库·后端·面试
小时前端4 小时前
🚀 面试必问的8道JavaScript异步难题:搞懂这些秒杀90%的候选人
javascript·面试
Takklin4 小时前
JavaScript 面试笔记:作用域、变量提升、暂时性死区与 const 的可变性
javascript·面试
知其然亦知其所以然4 小时前
面试官一开口就问:“你了解MySQL水平分区吗?”我当场差点懵了……
后端·mysql·面试
老马啸西风4 小时前
力扣 LC27. 移除元素 remove-element
算法·面试·github
南北是北北5 小时前
List排序/查找最佳实践
面试
南北是北北5 小时前
List视图与不可变
面试
绝无仅有5 小时前
面试技巧之Linux相关问题的解答
后端·面试·github
绝无仅有5 小时前
某跳动大厂 MySQL 面试题解析与总结实战
后端·面试·github