SWE · Phone Concurrency / Design Pattern

Apple Java Singleton:创建安全与共享状态安全是两回事

解释类初始化、静态内部类、volatile 双重检查以及单例内部可变状态的并发保护。

题目重点:解释类初始化、静态内部类、volatile 双重检查以及单例内部可变状态的并发保护。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 按 Java 内存模型和 JVM 类初始化语义讨论,不将其他语言的静态初始化规则类比过来。
  • ‘线程安全’拆成唯一实例创建与实例对共享可变字段的并发访问;题目未明确实现代码。

解题路线

  1. 先纠正前提:Singleton 是单实例意图,不自动保证线程安全;无同步的懒汉式 `if(instance==null) instance=new ...` 会竞态创建多个实例。
  2. 简单方案是静态 final 实例或静态内部 holder:类初始化由 JVM 协调,只初始化一次;构造阶段不泄露 `this`,实例引用安全发布。若需要惰性且简洁,enum singleton 也是常见选择,但需看序列化与框架需求。
  3. 双重检查锁方案必须让实例字段 `volatile`,否则可见性和重排问题;只有第一次创建加锁,后续走快路径。但通常优先采用更易审阅的 holder/enum。
  4. 即使实例只创建一次,内部 `HashMap`、计数器或缓存的并发读写仍须锁、并发容器或不可变快照。明确生命周期:进程内 singleton 不等于跨进程全局唯一,也不能代替数据库唯一约束。

手动走一遍

两个线程同时进入无锁懒汉 getInstance,都看见 null 并各自 new,违反唯一性;换 holder 可保证类初始化一次。但若返回的同一个对象里有 `count++`,两个线程仍可能丢更新,需要原子计数或同步。

规模与取舍

静态/holder 获取引用通常 O(1);锁定共享状态有竞争成本。单例是否适合还取决于测试替换、生命周期、依赖注入及全局可变状态耦合,而非性能优势。

易错点与追问

容易失分的边界

  • 说‘只有一个对象,所以天然线程安全’。
  • 无锁懒初始化未防并发创建。
  • 双重检查锁漏 `volatile`。
  • 构造函数把 `this` 发布给其他线程,使未完成对象可见。
  • 拿进程内单例来保证多节点唯一作业。

继续深挖

  • 静态内部类 holder 为什么能懒加载且唯一?
  • 实例有一个可变缓存时,创建安全后还需什么?
  • 什么时候依赖注入的作用域比 Singleton 模式更清晰?
  • 多进程共享资源唯一性应由哪一层保证?

技术参考

✓已复制微信号!