Kotlin Vocabulary:The one and only object
单例模式
单例模式是一个应用广泛的设计模式,详情参考:
单例模式
Kotlin 实现单例有一个优雅的方式 —— 使用 object
关键字
Java 的单例
要创建仅包含一个实例的类,需要将构造函数设为私有,并创建该对象的可公开访问的静态引用。然后提供一个静态方法来检查是否创建了对象。该方法必须返回先前创建的实例或调用构造函数并返回该实例。
public class Singleton{
private static Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
private int count = 0;
public int count(){ return count++; }
}
上面的代码看起来不错,但是有一个主要问题,这段代码不是线程安全的。多线程同时调用 getInstance() 时,可能破坏单例。原因见 下文中 编译优化带来的有序性问题 一节
😺 并发编程出现 bug 的源头
为解决这一问题,通常使用双重校验锁的形式
public class Singleton{
private static Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance(){
if (INSTANCE == null) { // Single Checked
synchronized (Singleton.class) {
if (INSTANCE == null) { // Double checked
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
private int count = 0;
public int count(){ return count++; }
}
这一过程略微繁琐,因此 Java 中大多数时候使用枚举来创建单例。
Kotlin 的单例
Kotlin 没有静态方法或者静态变量,在 Kotlin 中声明静态方法和变量可以通过伴生对象的形式(compaion object)
class Singleton private constructor() {
private var count = 0
fun count(): Int {
return count++
}
companion object {
private var INSTANCE: Singleton? = null// Double checked
// Single Checked
val instance: Singleton?
get() {
if (INSTANCE == null) { // Single Checked
synchronized(Singleton::class.java) {
if (INSTANCE == null) { // Double checked
INSTANCE =
Singleton()
}
}
}
return INSTANCE
}
}
}
或者我们也可以使用 object 关键字,让代码更简单
object Singleton {
private var count: Int = 0
fun count() {
count++
}
}
compaion object 背后实现
class Test {
companion object {
const val TEST = "TEST"
}
}
要查看 Kotlin 类的字节码,可以选择 Tools > Kotlin > Show Kotlin Bytecode 显示字节码后,点击 Decompile 转换为 Java 代码
public final class Test {
@NotNull
public static final String TEST = "TEST";
@NotNull
public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
可以看到,compaion object 是使用 静态内部类 Compaion
实现的。因此默认情况下 Java 调用 Kotlin 的代码方式是这样的:Test.Compaion.TEST
object 背后实现
object Singleton {
private var count: Int = 0
fun count() {
count++
}
}
要查看 Kotlin 类的字节码,可以选择 Tools > Kotlin > Show Kotlin Bytecode 显示字节码后,点击 Decompile 转换为 Java 代码
public final class Singleton {
private static int count;
@NotNull public static final Singleton INSTANCE;
public final void count() { int var10000 = count++; }
private Singleton() {}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
首次访问静态字段时,静态块仅被调用一次。JVM 处理静态块的方式与同步块类似,即使它们没有synchronized
关键字也是如此。在初始化此 Singleton 类时,JVM 会在同步块上获得一个锁,从而使另一个线程无法访问它。释放锁后,已经创建了 Singleton 实例,因此静态块将不再执行。这样可以保证只有一个 Singleton 实例。另外,该对象是线程安全的,并且在首次访问该对象时是延迟创建的。
为什么 static 代码块是线程安全的?
我们都知道,static 代码块只会在类加载时调用一次
那么我们可以做一个推断:如果类加载这个过程是线程安全的即意味着 static 代码块是线程安全的
Java 中类加载依靠 ClassLoader 中的 loadClass 方法 实现的,而该方法是被 synchronized
修饰的
🥁 双亲委托机制
作为验证,我们可以在 loadClass 方法上打入条件断点(过滤我们想要验证的类)
为什么 Kotlin 推荐使用 object 实现单例?
object 背后实现的写法有点像饿汉式的单例:
public class Single {
private Single() {
}
private static Single INSTANCE = new Single();
public static Single getInstance() {
return INSTANCE;
}
}
我们都知道 Java 的单例一般都使用懒汉式的双重校验锁的形式,那么为什么 Kotlin 使用的是 饿汉式呢?
这个问题很有意思,严格讲,object 不能等价于懒汉式。
object 中不能加入伴生对象:
从这个限制我们可以推断出 object 的用法,即当使用 object 时肯定是使用其成员。如果这么说比较抽象的话,我们可以对比一下 Java 饿汉式的写法:
public class Single {
private Single() {
}
+ public static void test() {
+
+ }
private static Single INSTANCE = new Single();
public static Single getInstance() {
return INSTANCE;
}
}
我们可以在 Single 中加入静态方法 test()
,当外部调用 test()
时,Single 实例便被创建出来,即使我们没调用 getInstance()
。
而 object 禁止使用伴生对象,因此我们在使用 object 类时便意味着调用其成员,因此 object 同懒汉式一样,是能够延迟加载的。
结论
- Kotlin 中可以使用 object 来实现单例
- compaion object 背后是通过名叫 Compaion 的静态内部类实现的
- object 背后是使用 java 静态代码块实现的
- object 背后实现的写法有点像饿汉式,但由于 object 类使用时只能调用它的某个成员,因此它的单例是 延迟加载 的(加载类 -> 执行静态代码块 -> 创建对象)
- 静态代码块是 线程安全 的,因为 ClassLoader#loadClass() 是线程安全的