饿汉式
1、object关键字
在Kotlin中饿汉式单例直接使用object关键字即可实现
object SingletonTest {
}
通过对以上代码的字节码反编译为java代码可以查看到
public final class SingletonTest {
public static final SingletonTest INSTANCE;
private SingletonTest() {
}
static {
SingletonTest var0 = new SingletonTest();
INSTANCE = var0;
}
}
可以看到这是一个标准的饿汉式单例类,注意,static代码块在类加载的初始化阶段执行,不同的jvm可能类加载的时机会有不同,例如Android平台的ART虚拟机只有在第一次访问类的静态资源或创建类的实例时才会进行类加载,也就是说在实际上并不会刚启动一个应用,就将内部的全部饿汉式单例类初始化赋值;由于static代码块是由虚拟机自动执行的,由虚拟机实现保证线程安全,因此通过object关键字实现的单例类天然就是线程安全的;
懒汉式
1、非线程安全的
class SingletonTest2 private constructor() {
companion object {
private var instance: SingletonTest2? = null
get() {
if (field == null) {
field = SingletonTest2()
}
return field
}
fun get(): SingletonTest2 {
return instance!!
}
}
}
2、线程安全的双重校验锁
class SingletonTest3 private constructor() {
companion object {
@Volatile
var instance: SingletonTest3? = null
private set
get() {
if (field == null) {
synchronized(this) {
if (field == null) {//代码1
instance = SingletonTest3()
}
}
}
return field
}
}
}
为什么要进行两次判空?第一次判空是出于性能考虑,如果每次调用get时都对整个类加同步锁会造成较高的性能开销。当多个线程同时调用get时,那么此时会有一个线程成功获取到同步锁并执行赋值操作,此时其他的线程均被阻塞,当获取到同步锁的那个线程赋值完成后并释放锁,此时其他被阻塞的线程将被唤醒,并执行到代码1处,如果此时不进行二次判空,那么又将会进行赋值,导致获取的单例对象不一致;
为什么要使用Volatile修饰instance?在java中volatile关键字的作用有
-
保证变量的可见性
在java内存模型中变量的存储分为主内存与工作内存,每一个线程都有自己单独的工作内存,所有对变量的操作都在工作内存中完成,在线程获得锁时重新从主内存中读取最新的值,线程释放锁时会将变量刷新回主内存中。
-
禁止指令重排
在虚拟机中一行代码可能对于多条虚拟机指令,例如instance = SingletonTest3()这一行代码实际包含分配内存、初始化对象、将instance指向刚刚分配的内存地址三条指令,以伪代码表示为
- memory = allocate() //分配内存
- ctorInstanc(memory) //初始化对象
- instance = memory //设置instance指向刚分配的地址
以上操作2和操作3可以互换,先将instance指向刚分配的地址再初始化对象,只要将instance指向一块内存空间后instance==null就会返回false,在多线程环境中,当一个线程正在执行步骤3,尚未执行步骤2时,若在此时另外一个线程调用get,那么就会直接返回instance,进而导致异常产生。Volatile可以禁止指令重排,从而使得严格按照步骤2、3的顺序执行。
在这里使用使用Volatile修饰instance的目的就在于禁止指令重排。
3、使用静态内部类实现的线程安全的
class SingletonDemo private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = SingletonDemo()
}
}
只有当访问instance时才会初始化SingletonHolder类,进而创建单例对象;holder的赋值在静态代码块中执行,由虚拟机保证了线程安全
4、使用Kotlin内置的lazy函数
class SingletonDemo private constructor() {
companion object {
val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SingletonDemo()
}
}
}
lazy函数的实现
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
可以看到lazy函数正是使用双重校验锁方式实现的。
综上所述,在实际项目中如果是饿汉式单例直接使用object即可,懒汉式单例推荐直接使用lazy关键字,写起来最方便