单例
总的来说,实现单例模式有下面几个关键点:
- 构造函数私有化,使外部不能直接构造单例对象;
- 暴露公共静态方法或枚举,返回单例对象;
- 确保线程安全,保证多线程环境下也仅有一个实例对象;
- 确保单例对象在反序列化时不会创建新的对象。
饿汉式单例:
饿汉式单例就是声明一个静态对象,在类被第一次加载的时候,就完成静态对象的实例化。
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12
| public class SingletonJava { private static final SingletonJava INSTANCE = new SingletonJava();
private SingletonJava() { }
public static SingletonJava getInstance() { return INSTANCE; } }
|
这种写法比较容易理解,也应该都有接触过,我们直接看一下如何在Kotlin实现类似效果。
Kotlin篇:
1
| object SingletonKotlin { }
|
没开玩笑,这真的是Kotlin的饿汉式单例,爽到难以置信。这种写法在Kotlin中有自己的称呼:对象声明。
- 何为对象声明,我们在
object关键字后面跟一个名称,就可以获取一个单例对象;
- 像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边;
- 而且对象声明的初始化过程是线程安全的。
如此神奇的效果,我们怎么能忍住不一探究竟,所以我们把Kotlin的自节码进行了反编译。
查看Kotlin的字节码:Tools → Kotlin → Show Kotlin Bytecode,然后点击字节码的DECOMPILE进行反编译:
1 2 3 4 5 6 7 8 9 10
| public final class SingletonKotlin { public static final SingletonKotlin INSTANCE;
private SingletonKotlin() { }
static { SingletonKotlin var0 = new SingletonKotlin(); INSTANCE = var0; } }
|
愣了,这分明就是我们饿汉式单例的另一种写法!难怪可以实现相同的单例效果,原来是Kotlin通过语法糖为我们做了简化封装。
懒汉式单例:
懒汉式单例就是声明一个静态对象,并且在用户第一次调用的时进行初始化。
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class SingletonJava {
private static SingletonJava INSTANCE;
private SingletonJava() { }
public static SingletonJava getInstance() { if (INSTANCE == null) { INSTANCE = new SingletonJava(); } return INSTANCE; } }
|
Kotlin篇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class SingletonKotlin private constructor() {
companion object { private var mInstance: SingletonKotlin? = null get() { field = field ?: SingletonKotlin() return field }
fun getInstance(): SingletonKotlin = mInstance!! } }
|
Java的写法大家都应该很熟悉了,而Kotlin的写法则有些不同,毕竟Kotlin也有它自己的语言特性:
被companion标记的这块代码,在Kotlin中被称为伴生对象:类似于Java的静态代码块,这样它就与外部类关联在一起,我们可以直接通过外部类访问到对象的内部元素。
而用过Kotlin的都知道,Kotlin的属性自带Getter和Setter(对于var属性,下同)。
所以我们在内部声明一个私有的mInstance,自定义它的Getter;当对象为空是进行实例化,当对象不为空时,直接返回实例对象。这样我们既满足了构造函数的私有化,有对外暴露了对象的获取方法。
小结一下:
- 懒汉式单例只有在使用时才会进行实例化,在一定程度上节约了资源;
- 但第一次加载时需要及时进行实例化,反应稍慢;
- 而且这种写法是非线程安全的,适用于单线程环境,不推荐使用。
线程安全的懒汉式:
为了解决上述线程安全性的问题,我们使用同步锁适应多线程的环境。
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class SingletonJava {
private static SingletonJava INSTANCE;
private SingletonJava() { }
public static synchronized SingletonJava getInstance() { if (INSTANCE == null) { INSTANCE = new SingletonJava(); } return INSTANCE; } }
|
Kotlin篇:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class SingletonKotlin private constructor() {
companion object { private var mInstance: SingletonKotlin? = null get() { field = field ?: SingletonKotlin() return field }
@Synchronized fun getInstance(): SingletonKotlin = mInstance!! } }
|
这种写法和上面的懒汉式写法类似,仅仅是多了一层同步锁,从而保证在多线程环境下的线程安全。由于每次对象的获取都会对整个类进行加锁,所以运行效率不高,实际使用中并不推荐。
作者:呱呱_
链接:https://www.jianshu.com/p/4e9dda5b486a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
双检锁
kotlin版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| final class SomeSingleton(context: Context) { private val mContext: Context = context companion object { @Volatile private var instance: SomeSingleton? = null fun getInstance(context: Context): SomeSingleton { val i = instance if (i != null) { return i }
return synchronized(this) { val i2 = instance if (i2 != null) { i2 } else { val created = SomeSingleton(context) instance = created created } } } } }
或者:
class SingletonKotlin private constructor() {
companion object { @Volatile private var mInstance: SingletonKotlin? = null get() { field = field ?: synchronized(this) { field ?: SingletonKotlin() } return field }
fun getInstance() = mInstance!! } }
|
封装一个带参单例
支持传参的单例,我们实现了。但为了实现这个单例,写了 20+ 行代码。每次写单例都要把这一堆代码复制一遍,还挺麻烦,为了使用方便,还可以将其再封装一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| open class SingletonHolder<out T, in A>(creator: (A) -> T) { private var creator: ((A) -> T)? = creator @Volatile private var instance: T? = null
fun getInstance(arg: A): T { val i = instance if (i != null) { return i }
return synchronized(this) { val i2 = instance if (i2 != null) { i2 } else { val created = creator!!(arg) instance = created creator = null created } } } fun getInstance2(arg: A): T = instance ?: synchronized(this) { instance ?: creator!!(arg).apply { instance = this } } }
|
用一个支持继承的 open class 加上泛型就可以简单的将其进行封装,此封装方式支持一个参数的构造方法,有需要可以继续扩展或者封装。
1 2 3 4 5 6 7 8
| class SomeSingleton private constructor(context: Context) { init { context.getString(R.string.app_name) }
companion object : SingletonHolder<SomeSingleton, Context>(::SomeSingleton) }
|
封装成 SingletonHolder 类之后,再想使用单例,关键代码一行就搞定了。
1 2 3 4 5 6 7 8
| class SomeSingleton private constructor(context: Context)
companion object : SingletonHolder<SomeSingleton, Context>(::SomeSingleton) }
|
1
| SomeSingleton.getInstance(context)
|
java版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton sInstance; private DoubleCheckSingleton(Context ctx) { } public static DoubleCheckSingleton getInstance(Context ctx) { if (sInstance == null) { synchronized (DoubleCheckSingleton.class) { if (sInstance == null) { sInstance = new DoubleCheckSingleton(ctx); } } } return sInstance; } }
|
这段代码的精髓就是getInstance方法的两层非空判断:
- 第一层为了避免不必要的同步,只需在第一次创建实例时同步;
- 第二层为了在对象为null的情况下创建实例,防止别的线程抢先初始化了。
可是仅靠getInstance方法就能解决并发线程安全性的问题吗?
我们一起看下INSTANCE = new SingletonJava()这行代码,这行代码都干了些什么呐,可基本拆分为三个步骤:
- 给
INSTANCE单例对象分配内存空间;
- 调用
SingletonJava()构造函数,初始化成员对象;
- 将
INSTANCE对象指向分配的内存空间(此时INSTANCE不为null)。
由于Java内存模型(JVM)允许指令重排,执行顺序不一定是1 → 2 → 3,也可能是1 → 3 → 2。
虽然单线程环境下,指令重排并不会影响最终结果,但会影响到多线程并发执行的正确性:
- 如果线程A按
1 → 3执行,还没有执行到2;
- 此时被切换带线程B,由于
INSTANCE已经不为null了,会被线程B直接取走;
- 而此时
INSTANCE是未完全初始化的,线程B直接使用将会出错。
结合并发线程安全的三要素:原子性、可见性、有序性。
我们知道这种写法是有缺陷的,可是怎么解决上述问题呐?答案就是我们上述代码用到的volatile关键字。
volatile可以禁止指令重排,保证有序性;
volatile还可以保证可见性,强制INSTANCE对象每次从主存中读取。
这里为什么不提原子性呐,因为原子性我们已经通过synchronized来保证了。关于volatile的更多内容,大家可以参考:Java并发编程:volatile关键字解析。
使用 lazy
前面在介绍带参单例的时候,也提到了lazy(),它是 Kotlin 的一种标准委托,可以接受一个 lambda 并返回一个实例的函数。
如果我们想要延迟初始化,可以使用 lazy() 这个代理来实现,它会在第一次调用get() 方法时,执行 lazy() 的 lambda 表达式并记录结果,之后再调用 get()就只会返回之前记录的结果,非常适合延迟初始化的场景。
1 2 3 4 5 6
| class SomeSingleton{ companion object { val instance: SomeSingleton by lazy { SomeSingleton() } } } 复制代码
|
lazy() 默认情况下,内部就是依赖同步锁(synchronized)来实现的,所以它也是线程安全的。
但是正如我前面提到的,类本身也是按需加载的,调用它的下一步肯定是也需要使用它,所以只要我们正确的使用单例模式,其实没必要使用 lazy(),这里仅做一个介绍,大家知道一下就好了。
委托属性:
val/var <属性名>: <类型> by <表达式>, by 后面的表达式就是该属性的委托。
属性的委托不需要实现接口,但是需要提供一个getValue()函数与 setValue()函数(对于var属性,下同)。效果类似于自定义的Getter和Setter,因为属性对应的get()、set()方法会被委托给它的getValue()、setValue() 方法。
百闻不如一见,我们还是写个简单的委托属性,大家一起看下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class MyStringDelegate {
private var value = "默认值2333..."
operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return value }
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { this.value = value println("我被赋值:$value") } }
|
getValue()、setValue()方法的参数比较拗口,为了避免手写出现错误,也可以通过继承ReadWriteProperty实现其中的方法,效果是一样的。
thisRef:为进行委托的类的对象,必须与属性所有者类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
property:为进行委托属性的对象本身,必须是类型KProperty<*>或其超类型;
value(setValue):必须与属性同类型或者是它的子类型。
1 2 3 4 5 6 7
| fun main(args: Array<String>) { var a: String by MyStringDelegate() println(a) a = "Hello World" println(a) }
|
运行上面的代码,我们得到了如下结果:
1 2 3
| 默认值2333... 我被赋值:Hello World Hello World
|
以上结果符合预期,因为在每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它:取值时将调用getValue()方法,赋值时将调用getValue()方法,这一点在查看反编译的字节码也可以证实。
静态内部类模式:
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class SingletonJava {
private SingletonJava() { }
public static SingletonJava getInstance() { return SingletonHolder.INSTANCE; }
private static class SingletonHolder { private static final SingletonJava INSTANCE = new SingletonJava(); } }
|
Kotlin篇:
1 2 3 4 5 6 7 8 9 10
| class SingletonKotlin private constructor() {
companion object { val instance = SingletonHolder.INSTANCE }
private object SingletonHolder { val INSTANCE = SingletonKotlin() } }
|
两种写法没什么差别,只是Kotlin看着更精简一些,那这种写法到底有什么好处呐:
首先,在第一次加载Singleton类时并不会初始化INSTANCE,只有在第一次调用getInstance()方法时才会进行初始化操作。而由于静态内部类的特性,在第一次调用getInstance()方法时,虚拟机会去加载SingletonHolder类;这种方式不仅能够保证线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。
总得来说,静态内部类的写法优点和DCL写法类似;而且不会被反射入侵,因为反射不能从外部类获取内部类的属性。不足就是需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其Class对象还是会被创建,而且是属于永久的对象。瑕不掩瑜,这种写法依旧是种比较推荐的单例实现方式。
Enum单例:
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class BusCore {
private BusCore() { }
public static BusCore getInstance() { return Singleton.INSTANCE.getInstance(); }
private static enum Singleton { INSTANCE; private BusCore classInstance;
Singleton() { classInstance = new BusCore(); }
public BusCore getInstance() { return classInstance; } } }
|
Kotlin篇:
1 2 3 4 5 6 7 8
| enum class SingletonEnumKotlin {
INSTANCE;
fun doSomething() { println("doSomething: Kotlin") } }
|
使用枚举来实现单例效果,确实是一种很骚的操作。枚举类与普通的类一样,也可以有自己的字段、方法,甚至实现一个或多个接口(Interface)。但枚举类不能作为子类继承其他类,也不能被继承,因为枚举反编译是final类型的。
使用枚举的究竟有何种优势呐,首先写法简单算一个;重要的是枚举实例的创建是线程安全的;而且在任何情况下它都是一个单例,即使反序列化也不会创建新的对象,而且JVM 还会阻止通过反射获取枚举类的私有构造方法。
容器单例模式:
Java篇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class SingletonManagerJava {
private static Map<String, Object> map = new HashMap<>();
public SingletonManagerJava() { }
public static void registerService(String key, Object instance) { if (!map.containsKey(key)) { map.put(key, instance); } }
public static Object getService(String key) { return map.get(key); } }
|
Kotlin篇:
1 2 3 4 5 6 7 8 9 10 11 12
| class SingletonManagerKotlin() {
private val map = mutableMapOf<String, Any>()
fun registerService(key: String, instance: Any) { if (!map.containsKey(key)) { map[key] = instance } }
fun getService(key: String) = map[key] }
|
在程序的初始,将多种单例类型注入到统一的管理类中,使用时根据key值获取对应的单例对象。使用这种方式我们可以管理多种类型的单例,并且可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
此种方式一般用于系统层面,如Android的系统服务就是通过此种方式进行管理,使用时可以通过getSystemService()方法获取具体的服务对象。