一个线程的写入何时被另一个线程看见,哪些操作不可分割,都需要规则。JMM 是 Java 对线程间内存行为的规范,不等于 JVM 堆栈划分。
三个问题
- 原子性:操作是否不可分割。count++ 包含读取、计算、写入,不是天然原子操作。
- 可见性:一个线程修改共享数据后,其他线程能否按同步规则看到结果。
- 有序性:编译器和处理器可以重排序,但必须遵守内存模型规定的可观察行为。
工作内存是解释线程交互的抽象,不代表每个线程都实际维护一份完整的主内存副本。
happens-before
happens-before 表示规范承诺的可见性和顺序关系,不只是现实时间上先发生。常用规则是:
- 同一线程内,前面的操作 happens-before 后面的操作。
- 对一个监视器的解锁 happens-before 后续对同一监视器的加锁。
- 对 volatile 变量的写 happens-before 后续对该变量的读。
- 调用 start 前的操作对启动后的线程可见。
- 线程中的操作对成功 join 等待它结束的线程可见。
- 关系具有传递性。
重点是能用规则解释为何可见,不需要背所有规范条文。
volatile 能保证什么?
volatile 适合状态标记和满足条件的安全发布,建立相关读写的可见性与排序约束,不能让多个步骤组成原子操作。
volatile boolean running = true;volatile int count = 0;// running 可以作为协作停止标记// count++ 仍可能丢失更新计数累加可以使用 AtomicInteger,或用同一把锁保护。volatile 引用也不自动保护对象内部的并发修改。
synchronized 锁住什么?
实例同步方法通常锁 this;静态同步方法锁对应 Class 对象;同步块锁括号中指定对象。只有竞争同一个监视器的操作才会互斥。
锁同时提供互斥和同步可见性,但共享状态的相关读写要遵守同一套协议。写时加锁,读时完全无同步,不能仅靠写端加锁保证正确。
双重检查单例为什么需要 volatile?
双重检查在锁外读取实例引用,锁内再确认并创建。实例引用使用 volatile,保证安全发布,避免其他线程看到引用后不能正确观察已初始化状态。
final class DclSingleton { private static volatile DclSingleton instance;
private DclSingleton() {}
public static DclSingleton getInstance() { if (instance == null) { synchronized (DclSingleton.class) { if (instance == null) { instance = new DclSingleton(); } } } return instance; }}外层检查避免每次调用都加锁,内层检查防止多个线程依次进入锁后重复创建,volatile 负责实例的安全发布。缺少任意一层相应保障,都不能只凭“用了 synchronized”就认定这个模式正确。
常见线程安全单例怎么选?
| 方式 | 机制 | 创建时机与取舍 |
|---|---|---|
| 饿汉式 | 静态字段在类初始化中创建,利用类初始化保证 | 类初始化时创建,不是必然在应用启动那一刻 |
| 双重检查 | 锁控制创建,volatile 安全发布 | 按需创建,但手写逻辑较多 |
| 静态内部类 | 内部 Holder 的静态字段通过类初始化创建 | 常规访问 getInstance 时触发,简洁的延迟创建方案 |
| 枚举 | 枚举常量与类初始化机制 | 常规枚举语义下单例身份稳定,并有序列化等方面优势 |
final class HolderSingleton { private HolderSingleton() {}
private static class Holder { private static final HolderSingleton INSTANCE = new HolderSingleton(); }
public static HolderSingleton getInstance() { return Holder.INSTANCE; }}
enum EnumSingleton { INSTANCE}这些方式主要保障实例创建和发布,不保证实例内所有可变业务状态线程安全。类加载器隔离也可能产生多个类身份,不能把单例理解为任何环境下全 JVM 绝对一个对象。Spring 的 singleton 则首先是容器中 Bean 定义的作用域,与手写单例模式不同。
面试回答模板
JMM 定义线程间共享内存的可见性和排序规则。volatile 保证相关读写的可见性和顺序,但不保证 count++ 等复合操作原子。synchronized 使用同一对象监视器互斥,并建立解锁到后续加锁的 happens-before。简单状态发布可用 volatile,多步一致更新通常需要锁或合适的原子 API。
自测
- volatile int 做 ++ 安全吗?不安全。
- 实例同步方法与静态同步方法一定使用同一把锁吗?不是。
- volatile List 引用能保证并发 add 安全吗?不能。
- 静态内部类单例为什么不需要手写 volatile?利用类初始化的同步和可见性保证。