
Java 中对象引用的赋值操作本身是原子的,但缺乏内存可见性保证;若存在一个写线程(定时更新)与多个读线程(如 Web 请求),必须通过同步机制(如 synchronized 或 volatile)确保读线程能及时看到最新引用,否则可能长期缓存旧值。
java 中对象引用的赋值操作本身是原子的,但缺乏内存可见性保证;若存在一个写线程(定时更新)与多个读线程(如 web 请求),必须通过同步机制(如 `synchronized` 或 `volatile`)确保读线程能及时看到最新引用,否则可能长期缓存旧值。
在 Spring 应用中,当一个组件(如 ExampleA)被多个请求线程并发读取,同时又被 @Scheduled 任务单线程定期更新时,看似简单的引用替换(listFromDynamo = result)会引发可见性问题——这并非线程安全问题(不会导致 JVM 崩溃或数据损坏),而是JMM(Java 内存模型)导致的缓存不一致:写线程更新了字段,但读线程可能永远看不到该更新,因为它一直使用自己 CPU 缓存中的旧值。
✅ 为什么 listFromDynamo = result 是原子的,却仍不安全?
- 引用赋值(64 位以下 JVM 中为 32 位指针,64 位 JVM 默认开启压缩指针后也为 32 位)在 JVM 层面是原子操作,不会出现“半截指针”;
- 但 Java 不保证该写入对其他线程立即可见。JVM 允许每个线程维护字段的本地副本(基于 CPU cache 和编译器重排序优化),除非显式建立 Happens-Before(HB)关系。
例如,若未加同步:
// 线程 A(更新) listFromDynamo = new ArrayList(freshData); // ✅ 原子写入 // 线程 B(读取,可能持续数小时/数天) return listFromDynamo; // ❌ 可能始终返回 null 或旧列表(即使 A 已执行多次)
这种行为不可预测、不可测试、不可移植——在开发机上看似正常,上线后却因硬件/VM 参数差异而失效。
✅ 正确做法:建立 Happens-Before 关系
最简洁、可靠且符合 Spring 实践的方式是使用 synchronized 块保护读写操作:
@Component
public class ExampleA {
// ... DynamoDB client 初始化(略)
private List<classfromdynamo> listFromDynamo;
private final Object dynamoLock = new Object(); // 推荐使用私有 final 锁对象
public List<classfromdynamo> getListFromDynamo() {
synchronized (dynamoLock) {
return listFromDynamo;
}
}
public void updateListFromDynamo() {
List<classfromdynamo> result = new ArrayList();
table.scan().forEach(page -> result.addAll(page.items()));
synchronized (dynamoLock) {
listFromDynamo = result; // ✅ 写入与后续所有读操作建立 HB 关系
}
}
}</classfromdynamo></classfromdynamo></classfromdynamo>
? 关键点:
synchronized不仅互斥执行,更重要的是它在进入/退出时插入内存屏障(memory barrier),强制刷新本地缓存并同步主内存状态,从而满足 JMM 对 HB 的定义。
⚠️ 注意事项与替代方案
不要锁
this或公有对象(如synchronized(this)):易引发外部死锁或意外竞争;volatile单独不足以解决问题:虽然volatile List<...> listFromDynamo</...>能保证引用可见性,但它不保证列表内容的线程安全(如getListFromDynamo().size()期间列表被另一线程修改,仍可能出错)。本场景中仅需替换整个列表引用,因此volatile技术上可行,但语义不如synchronized清晰,且无法扩展支持未来可能的复合操作(如“先查再更新”);-
AtomicReference<list>></list>是更轻量的替代:private final AtomicReference<list>> listRef = new AtomicReference(); public List<classfromdynamo> getListFromDynamo() { return listRef.get(); // ✅ volatile 语义 + 原子操作 } public void updateListFromDynamo() { List<classfromdynamo> result = ...; listRef.set(result); // ✅ 同样建立 HB }</classfromdynamo></classfromdynamo></list>性能略优(无锁),代码更简洁,推荐用于纯引用替换场景。
避免过度优化:表扫描(
table.scan())本身是 I/O 密集型高开销操作,同步块带来的微小竞争成本可忽略不计。优先保障正确性,再用 Profiler(如 Async-Profiler)验证瓶颈。
✅ 总结
| 场景 | 是否需要同步 | 推荐方案 |
|---|---|---|
| 单写多读,仅替换整个引用 | ✅ 必须(否则不可见) |
synchronized(清晰稳健) 或 AtomicReference(轻量精准) |
| 需要读-改-写复合逻辑 | ✅ 必须 |
synchronized(支持临界区扩展) |
| 仅读取,且引用永不变更 | ❌ 不需要 | — |
最终原则:原子性 ≠ 可见性 ≠ 线程安全。在并发环境下,只要存在跨线程的数据共享,就必须主动建立 Happens-Before 关系——这是编写可信赖 Java 并发代码的基石。










