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) 决策树(面试/落地小抄)
- 读多写少? → CopyOnWriteArrayList(监听器)或 快照读取。
- 强一致? → synchronizedList + 同锁遍历,或 ReadWriteLock。
- 需要高吞吐并行? → 换并发容器(队列/映射),别勉强用 List。
- 架构允许? → Actor/消息化,不共享可变结构。
- UI 列表? → ViewModel 暴露不可变快照,ListAdapter+DiffUtil 增量刷新。
结论
默认策略 :不共享可变 List ,对外只给快照/不可变 ;需要共享时,要么加锁 ,要么用合适的并发容器 ;监听器用 COW ;实时场景优先消息化/所有权。这样既稳又好维护。