单例模式

Catalogue   

单例模式应该是最常见的设计模式了

定义

定义:单例对象的类必须保证只有一个实例存在。

为了实现一个健壮的单例,我们应该思考需要做哪些事情?

  • 不能随意的让用户new出对象,所以构造函数应该是私有的
  • 既然不能直接new,就应该有一个方法专门用来返回实例对象
  • 不能clone
  • 不能被反序列化
  • 多线程使用时,如何保证线程安装

Java的实现方式

根据上面的思考,我们可以一步步的来实现单例模式。

懒汉式

1
2
3
4
5
6
7
8
9
10
public class Singleton { 
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

使用的时候才进行初始化,但此种写法是线程不安全的。那么是否把getInstance方法加一个锁就可以了呢?

线程安全的懒汉式

1
2
3
4
5
6
7
8
9
10
public class Singleton { 
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

嗯,这种写法确实安全了,可是效率低,因为有更好的方式啊

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton { 
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() { return instance; }
}

public class Singleton {
private static Singleton instance = null;
static { instance = new Singleton(); }
private Singleton (){}
public static Singleton getInstance() { return instance; }
}

为什么叫饿汉呢?因为在类加载的时候对象就被创建了啊!这也是该方式不太好的地方,需要的实例应该要在需要用到的时候才初始化呢!
所以应该想想怎样才能延迟加载呢?

静态内部类实现方式

1
2
3
4
5
6
7
8
9
public class Singleton { 
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。完美!!

枚举

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 enum Singleton {
INSTANCE;
public void whoAmI() { System.out.println(this.toString()); }
}

//方式二
public class Singleton {
private Singleton(){
}
public void whoAmI() { System.out.println(this.toString()); }
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}

}

Java虚拟机会保证枚举类型不能被反射并且构造函数只被执行一次

双重检查锁模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private Singleton(){}

private volatile static Singleton instance = null;

public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}

}

为什么要这样写呢?

其实这是懒汉式的升级版。懒汉式中所有线程在访问getInstance都会锁住,但实际情况中,很多时候只是读操作,
我们没有必要让每个线程都锁住才调用它。

为什么要加volatile呢?

因为JVM指令可重排,具体可以看看volatile这篇文档

为什么要进行两次非空判断呢?

第一次校验: 也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

第二次校验: 也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法。

同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。

此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

AtomicReference实现

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
42
43
44
45
46
47
48
49
50
51
52
53
package com.shjlone.singleton;

import java.util.concurrent.atomic.AtomicReference;

/**
* 使用AtomicReference的单例
*/
public class AtomicReferenceSingleton {

private static final AtomicReference<Object> instance;

private AtomicReferenceSingleton() {
}


static {
instance = new AtomicReference<>();
}

public static AtomicReferenceSingleton getInstance() {
Object value = AtomicReferenceSingleton.instance.get();
if (value == null) {
synchronized (AtomicReferenceSingleton.instance) {
value = AtomicReferenceSingleton.instance.get();
if (value == null) {
final AtomicReferenceSingleton actualValue = new AtomicReferenceSingleton();
value = ((actualValue == null)) ? AtomicReferenceSingleton.instance : actualValue;
AtomicReferenceSingleton.instance.set(value);
}
}
}
return (AtomicReferenceSingleton)((value == AtomicReferenceSingleton.instance) ? null : value);
}


public static void main(String[] args) {
System.out.println(AtomicReferenceSingleton.getInstance());
System.out.println(AtomicReferenceSingleton.getInstance());
System.out.println(AtomicReferenceSingleton.getInstance());

for(int i=0; i<10; i++) {
new Thread() {
@Override
public void run() {
super.run();
System.out.println(AtomicReferenceSingleton.getInstance());
}
}.start();
}

}
}

相较于synchronize,CAS的实现性能更高。

Kotlin的实现方式

由于kotlin的语言特性,有些写法还是有别于Java的。

饿汉式

1
object Singleton {}

对!就是这么简单。以上代码转换成Java后,就是在静态代码块中初始化了Singleton。

懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton private constructor() {
companion object {
private var instance: Singleton? = null
get() {
if (field == null) {
field = Singleton()
}
return field
}

fun get(): Singleton {
//这里不用getInstance作为为方法名,是因为在伴生对象声明时,内部已有getInstance方法,所以只能取其他名字
return instance!!
}
}
}

线程安全的懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton private constructor() {

companion object {
private var instance: Singleton? = null
get() {
if (field == null) field = Singleton()
return field
}

@Synchronized
fun instance(): Singleton {
return instance!!
}
}
}

双重锁校验

1
2
3
4
5
class Singleton private constructor() {
companion object {
val instance by lazy { Singleton() }
}
}

静态内部类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton private constructor() {
companion object {

@JvmStatic //方便Java中使用
fun getInstance() {
return Holder.instance
}

}

private object Holder {
val instance = Singleton()
}

}

1
2
3
4
enum class Singleton {
INSTANCE;
}

如何处理反序列化

最开始的分析指出,为了不让别人直接new,构造函数需要设置成私有的。除此之外,还可能被反序列化。那么怎么解决这个问题呢?

1
2
3
4
5
6
7
public class Singleton implements Serializable {
private final static Singleton instance;
private Object readResolve()
{
return instance;
}
}

这样就行了?可以参考https://www.jianshu.com/p/ea1d9bc40341这篇文章了解原理。

如何防止反射

可以在私有构造函数中加一些限制条件,比如判断静态变量是否已经存在了

参考