单例模式

什么是单例模式?
单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。
特点:

  1. 一个类只能有一个实例
  2. 自己创建这个实例
  3. 整个系统都要使用这个实例

什么时候用单例模式?

用我的话说

  1. 只用一个实例就能解决问题的时候,没必要创建多个实例的时
    创建数据库连接池对象一个就够了
  2. 操作打印机的时候
    一台打印机总不能让所有人同时去操作,确保只有一个实例,每次只能被一个人调用该实例。

饿汉模式

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程并发问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。
如果从始至终从未使用过这个实例,则会造成内存的浪费。

但是说人话,如果这个对象最终一定会被使用到,最好是用饿汉模式,避免线程并发问题。
但是如果就是想追求这么点性能要求,也不是不可以用懒汉模式。

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

public static Singleton getInstance(){
return INSTANCE;
}
}

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
优缺点和上面是一样的。

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

private Singleton() {}

public Singleton getInstance() {
return instance;
}
}

懒汉模式

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。
如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
所以在多线程环境下不可使用这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton singleton;
private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
//A B 争抢会有并发问题
singleton = new Singleton(B);
}
return singleton;
}
}

改进,双重检查

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
/**
* 懒汉模式的特点:实例的延迟加载。
* 问题是:多线程时会有同步问题。解决方式:加锁。
*/
public class Hungry {

//1.创建空的对象,用于延迟加载。
//2.私有化构造器
//3.给出获取方法。但是为了防止出现线程安全问题,必须采取双得检查加同步代码块的方式进行安全保护机制。
private static Hungry hungry = null;

private Hungry() {

}

public static Hungry getInstance() {
if (hungry == null) {
synchronized (Hungry.class) {
//这层检查是必须要加的,如果没加,那么另一个线程进来后如果出现抢线程的情况那么对象将会被创建二个。
if (hungry == null) {
hungry = new Hungry();
}
}
}
return hungry;
}
}

双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些失败的一个主要原因。

所以,使用加上volatile关键字,防止指令重排:

1
private volatile static Hungry hungry = null;

到这懒汉模式算是一个可用的懒汉模式。但是单例械式还有别的形态。

静态内部类

有没有一种延时加载,并且能保证线程安全的简单写法呢?
我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

1
2
3
4
5
6
7
8
9
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}

但是,上面提到的所有实现方式都有两个共同的缺点:

都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
我就干过。

枚举写法

静态内部类,可以保证单例,但是不保证单例安全。
使有枚举可以。
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
因此,<Effective Java>推荐尽可能地使用枚举来实现单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Singleton {

INSTANCE;

public void doWork() {
System.out.println("work");
}

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Singleton.INSTANCE.doWork();
// 反射
Class<Singleton> clazz = Singleton.class;
Singleton singleton = clazz.newInstance();
singleton.doWork();

}
}

使用反射的报错:

Exception in thread "main" java.lang.InstantiationException: com.liukai.test.Singleton
at java.lang.Class.newInstance(Class.java:427)
at com.liukai.test.Singleton.main(Singleton.java:15)
Caused by: java.lang.NoSuchMethodException: com.liukai.test.Singleton.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more