
Java 中对引用类型的赋值操作本身是原子的,但缺乏内存可见性保证;多线程下仅靠赋值无法确保读线程及时看到写线程的最新值,必须借助同步机制(如 synchronized 或 volatile)建立 Happens-Before 关系。
java 中对引用类型的赋值操作本身是原子的,但缺乏内存可见性保证;多线程下仅靠赋值无法确保读线程及时看到写线程的最新值,必须借助同步机制(如 `synchronized` 或 `volatile`)建立 happens-before 关系。
在 Spring 应用中,常见场景是:一个组件(如 ExampleA)持有一个从外部数据源(如 AWS DynamoDB)加载的只读列表,该列表由定时任务(@Scheduled)每 10 分钟刷新一次,同时被多个 Web 请求线程并发读取。这种“一写多读”模式看似简单,却极易因 Java 内存模型(JMM)的缓存行为引发可见性问题——即写线程已更新了引用,但读线程仍长期持有旧值,且该问题不可预测、不可复现、难以测试。
✅ 引用赋值是原子的,但不保证可见性
Java 规范明确指出:对引用类型(以及 long/double 以外的基本类型)的赋值是原子操作。这意味着你不会读到“半截指针”或发生 NullPointerException 因为对象正在被构造中——但原子性 ≠ 可见性。JVM 允许每个线程维护字段的本地副本(例如 CPU 缓存或寄存器),若无同步约束,写入可能永远不被其他线程观察到。
// 危险示例:无同步的读写
private List<classfromdynamo> listFromDynamo;
public List<classfromdynamo> getListFromDynamo() {
return listFromDynamo; // ❌ 可能持续返回旧引用
}
public void updateListFromDynamo() {
List<classfromdynamo> result = table.scan()
.stream()
.flatMap(page -> page.items().stream())
.collect(Collectors.toList());
listFromDynamo = result; // ✅ 原子赋值,但❌ 不保证其他线程可见
}</classfromdynamo></classfromdynamo></classfromdynamo>
✅ 正确方案:建立 Happens-Before 关系
要确保 updateListFromDynamo() 的写入对 getListFromDynamo() 的读取可见,必须在两者间建立 Happens-Before(HB)关系。最直接、可靠且易于维护的方式是使用 synchronized 块(推荐私有锁对象,避免锁膨胀或外部干扰):
@Component
public class ExampleA {
// ... DynamoDB 客户端初始化代码 ...
private List<classfromdynamo> listFromDynamo;
private final Object dynamoLock = new Object(); // 私有锁对象,轻量且安全
public List<classfromdynamo> getListFromDynamo() {
synchronized (dynamoLock) {
return listFromDynamo; // ✅ 读取前获取锁 → HB 于上次写入
}
}
public void updateListFromDynamo() {
List<classfromdynamo> result = new ArrayList();
table.scan().forEach(page -> result.addAll(page.items()));
synchronized (dynamoLock) {
listFromDynamo = result; // ✅ 写入后释放锁 → HB 于后续所有读取
}
}
}</classfromdynamo></classfromdynamo></classfromdynamo>
? 为什么用私有锁对象?
避免使用this或公开对象(如listFromDynamo)作为锁,防止外部代码意外加锁导致死锁或性能瓶颈。new Object()是零开销、语义清晰的最佳实践。
✅ 替代方案:AtomicReference(适合高吞吐低延迟场景)
若经压测确认 synchronized 成为瓶颈(通常不会,因 DynamoDB 扫描本身已是 I/O 密集型慢操作),可改用 AtomicReference,它通过底层 CAS 指令提供无锁的可见性与原子性:
private final AtomicReference<list>> listRef
= new AtomicReference(Collections.emptyList());
public List<classfromdynamo> getListFromDynamo() {
return listRef.get(); // ✅ volatile 语义,天然 HB
}
public void updateListFromDynamo() {
List<classfromdynamo> result = table.scan()
.stream()
.flatMap(page -> page.items().stream())
.collect(Collectors.toList());
listRef.set(result); // ✅ volatile 写入,立即对所有线程可见
}</classfromdynamo></classfromdynamo></list>
⚠️ 注意:AtomicReference 仅保障引用本身的可见性与原子更新,不保护其内部元素的线程安全。本例中 List 仅用于只读访问,因此完全适用。
✅ 最佳实践总结
- 不要依赖“引用赋值原子性”来规避同步:它解决不了可见性问题;
-
优先使用
synchronized+ 私有锁对象:简单、可靠、可读性强,且现代 JVM 对无竞争锁做了极致优化(偏向锁 → 轻量级锁 → 重量级锁); -
仅当实测存在性能瓶颈时,再考虑
AtomicReference:它更轻量,但语义稍弱(不支持复合操作如 compare-and-set 条件更新); -
禁止使用
volatile直接修饰List字段:虽然volatile List能解决可见性,但易被误用(如list.add()非原子),且不如封装在AtomicReference中语义清晰; -
避免在 getter/setter 上直接加
synchronized方法:会锁住整个实例,限制扩展性;显式锁对象更灵活可控。
最终,线程安全不是“要不要加锁”的选择题,而是“如何正确建立内存同步契约”的工程实践。一次严谨的同步设计,远胜于千次侥幸的测试通过。










