线程安全的单例

这是为你整理的最终技术文档。文档聚焦于 Kotlin 顶级函数实现 DCL 单例的四种演进方案,重点标注了面试中必须指出的致命错误。


Kotlin 单例模式(DCL)最终技术文档

核心背景

在禁用 objectcompanion object 的情况下,必须使用 Top-level(顶级) 变量与函数。此时,变量编译后等同于 Java 的 static 成员。


情况一:❌ 致命错误版(最常见的挂点)

1
2
3
4
5
6
7
8
9
10
11
12
13
private var instance: Singleton? = null // 错误 1:缺失 @Volatile
private val lock = Any()

fun getInstance(): Singleton {
if (instance == null) { // 第一次检查
synchronized(Any()) { // 错误 2:锁不唯一,每次都 new 一个新锁
if (instance == null) {
instance = Singleton()
}
}
}
return instance!!
}

致命缺陷指出:

  1. 指令重排风险:缺少 @Volatile,导致线程 A 可能先赋值引用,后执行构造函数。线程 B 在锁外检查到 instance 非空,拿走一个未初始化的“半成品”对象,引发崩溃。
  2. 同步失效synchronized(Any()) 导致每个线程都在拿不同的钥匙,无法形成互斥,会创建出多个单例。

情况二:❌ 性能低效版(判空仅在锁内)

1
2
3
4
5
6
7
8
9
10
11
12
@Volatile
private var instance: Singleton? = null
private val lock = Any()

fun getInstance(): Singleton {
synchronized(lock) { // 错误 3:缺失外部判断
if (instance == null) {
instance = Singleton()
}
}
return instance!!
}

缺陷指出:

  • 性能瓶颈:虽然线程安全,但由于没有外部判空,每一次调用该方法都要排队竞争锁。在高并发场景(如 Android 列表滑动、数据库频繁读写)下会造成严重卡顿。

情况三:❌ 锁范围错误版(成员变量锁)

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton private constructor() {
private val lock = Any() // 错误 4:锁定义在类内部
}

@Volatile
private var instance: Singleton? = null

fun getInstance(): Singleton {
return instance ?: synchronized(lock) { // 无法访问,且锁不唯一
instance ?: Singleton().also { instance = it }
}
}

缺陷指出:

  • 作用域冲突:顶级函数(静态上下文)无法访问类内部的成员变量 lock
  • 逻辑悖论:单例创建前实例并不存在,实例不存在则 lock 不存在。必须使用静态锁才能保证创建过程的唯一性。

情况四:✅ 最终正确版(标准 DCL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Volatile // 必须:禁止指令重排序,保证锁外线程安全
private var instance: Singleton? = null

private val lock = Any() // 必须:全局唯一静态锁

class Singleton private constructor()

fun getInstance(): Singleton {
// 第一次判空:为了性能,避免不必要的锁竞争
return instance ?: synchronized(lock) {
// 第二次判空:为了安全,防止排队线程重复创建
instance ?: Singleton().also { instance = it }
}
}

💡 面试核心知识点总结

关键组件 核心作用
@Volatile 保护锁外线程。防止其读取到“已赋值地址但未初始化完成”的对象(解决指令重排)。
静态唯一锁 保证互斥性。确保所有线程竞争同一把钥匙,防止创建多个实例。
外层判空 性能优化。单例一旦创建,后续调用实现“零开销”返回。
内层判空 原子性保障。拦截那些在锁外排队的线程,确保只执行一次构造函数。
,