题目重点:解释类初始化、静态内部类、volatile 双重检查以及单例内部可变状态的并发保护。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 按 Java 内存模型和 JVM 类初始化语义讨论,不将其他语言的静态初始化规则类比过来。
- ‘线程安全’拆成唯一实例创建与实例对共享可变字段的并发访问;题目未明确实现代码。
解题路线
- 先纠正前提:Singleton 是单实例意图,不自动保证线程安全;无同步的懒汉式 `if(instance==null) instance=new ...` 会竞态创建多个实例。
- 简单方案是静态 final 实例或静态内部 holder:类初始化由 JVM 协调,只初始化一次;构造阶段不泄露 `this`,实例引用安全发布。若需要惰性且简洁,enum singleton 也是常见选择,但需看序列化与框架需求。
- 双重检查锁方案必须让实例字段 `volatile`,否则可见性和重排问题;只有第一次创建加锁,后续走快路径。但通常优先采用更易审阅的 holder/enum。
- 即使实例只创建一次,内部 `HashMap`、计数器或缓存的并发读写仍须锁、并发容器或不可变快照。明确生命周期:进程内 singleton 不等于跨进程全局唯一,也不能代替数据库唯一约束。
手动走一遍
两个线程同时进入无锁懒汉 getInstance,都看见 null 并各自 new,违反唯一性;换 holder 可保证类初始化一次。但若返回的同一个对象里有 `count++`,两个线程仍可能丢更新,需要原子计数或同步。
规模与取舍
静态/holder 获取引用通常 O(1);锁定共享状态有竞争成本。单例是否适合还取决于测试替换、生命周期、依赖注入及全局可变状态耦合,而非性能优势。
易错点与追问
容易失分的边界
- 说‘只有一个对象,所以天然线程安全’。
- 无锁懒初始化未防并发创建。
- 双重检查锁漏 `volatile`。
- 构造函数把 `this` 发布给其他线程,使未完成对象可见。
- 拿进程内单例来保证多节点唯一作业。
继续深挖
- 静态内部类 holder 为什么能懒加载且唯一?
- 实例有一个可变缓存时,创建安全后还需什么?
- 什么时候依赖注入的作用域比 Singleton 模式更清晰?
- 多进程共享资源唯一性应由哪一层保证?
技术参考
- Java 语言规范 §12.4.2:类初始化的并发协调规则。
