单例

单例

总的来说,实现单例模式有下面几个关键点:

  • 构造函数私有化,使外部不能直接构造单例对象;
  • 暴露公共静态方法或枚举,返回单例对象;
  • 确保线程安全,保证多线程环境下也仅有一个实例对象;
  • 确保单例对象在反序列化时不会创建新的对象。

饿汉式单例:

饿汉式单例就是声明一个静态对象,在类被第一次加载的时候,就完成静态对象的实例化。

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的字节码:ToolsKotlinShow 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() {

// 伴生对象,类似于Java的静态代码块
companion object {
// 声明私有对象,并重写get方法
private var mInstance: SingletonKotlin? = null
get() {
// 如果对象为空,则进行实例化
field = field ?: SingletonKotlin()
return field
}

// 对外暴露获取单例对象的接口
fun getInstance(): SingletonKotlin = mInstance!!
}
}

Java的写法大家都应该很熟悉了,而Kotlin的写法则有些不同,毕竟Kotlin也有它自己的语言特性:

companion标记的这块代码,在Kotlin中被称为伴生对象:类似于Java的静态代码块,这样它就与外部类关联在一起,我们可以直接通过外部类访问到对象的内部元素。

而用过Kotlin的都知道,Kotlin的属性自带GetterSetter(对于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 {
// Init using context argument
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) {
init {
// Init using context argument
context.getString(R.string.app_name)
}

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) {
// init
}
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()这行代码,这行代码都干了些什么呐,可基本拆分为三个步骤:

  1. INSTANCE单例对象分配内存空间;
  2. 调用SingletonJava()构造函数,初始化成员对象;
  3. 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属性,下同)。效果类似于自定义的GetterSetter,因为属性对应的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>) {
// 将 a 的值委托给 MyStringDelegate
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()方法获取具体的服务对象。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!