单例模式

单例模式是java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式设计到一个单一的类,该类负责创建自己的对象,同时保证只有单个对象被创建,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

概述

意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当你想控制实例数目,节省系统资源的时候。

实现

  • 构造函数私有。
  • 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

优点

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;
  • 避免对资源的多重占用。

缺点

  • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

代码示例

创建一个Singleton类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

//创建一个Singleton对象
private static Singleton instance = new Singleton();

//获取唯一可以用对象
public static Singleton getInstance(){
return instance;
}

//让构造函数私有化,这样改类就不能在外部实例化
private Singleton() {
}
}

从Singleton类中获取唯一对象

1
2
3
4
5
6
7
8
9
10
11
public class Test {

public static void main(String[] args) {
//无法通过new调用
//Singleton singleton = new Singleton();

//获取唯一可用对象
Singleton instance = Singleton.getInstance();
}

}

UML类图

单例模式的几种实现方式

1.饿汉式(静态变量)

是否懒加载:
是否线程安全:
描述: 这种方式比较常用,但是容易产生垃圾。
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存空间。
它基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。

1
2
3
4
5
6
7
8
9
10
public class Singleton {

private static Singleton instance = new Singleton();

public static Singleton getInstance(){
return instance;
}

private Singleton() { }
}

2.饿汉式(静态代码块)

是否懒加载:
是否线程安全:
描述: 这种方式比较常用,但是容易产生垃圾。
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存空间。
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候就执行静态代码块中的代码,初始化类的实例。优点和缺点和上面也是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton instance;

static {
instance = new Singleton();
}

public static Singleton getInstance() {
return instance;
}

}

3.懒汉式,线程不安全

是否懒加载:
是否线程安全:
描述: 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronize,所以严格意义上它并不算单例模式。这种方式懒加载很明显,不要求线程安全,在多线程不能正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton instance;

private Singleton() { }

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

4.懒汉式,线程安全

是否懒加载:
是否多线程安全:
描述: 这种方式具备很好的lazy loading,能够在多线程中很好的工作,但是效率很低,99%情况下不需要同步。
优点: 第一次调用才初始化,避免内存浪费。
缺点: 必须加锁synchronize才能保证单例,但加锁会影响效率。
getInstance()的性能对应用程序不是很关键(该方法使用不太频繁)。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton instance;

private Singleton() { }

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

5.双重锁/双重校验锁

是否懒加载:是
是否多线程安全:是
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance()的性能对应用程序很关键,参数需要添加volatile关键字防止指令重排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance;

private Singleton() { }

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

6.登记式/静态内部类

是够懒加载:
是否多线程安全:
描述: 这种方式能达到双检锁方式一样的效果,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式,这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式利用了classloader机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是:饿汉式只要类被装载了,那么instance就会被实例化(没有达到懒加载的效果),而这种方式是Singleton类被装载了,instance不一定被初始化,因为SingletonHolder类没有被主动使用,自由通过显式调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。

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

public Singleton() { }

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

7.枚举

是否懒加载:
是否多线程安全:
描述: 这种方式还没有被广泛使用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

通过反射或序列化破解单例

反射破解

单例类(以双检锁为例):

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

private static volatile Singleton instance;

private Singleton(){}

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

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

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Singleton instance = Singleton.getInstance();
System.out.println(instance);
Class c1 = instance.getClass();
Constructor constructor1 = c1.getDeclaredConstructor(null);
constructor1.setAccessible(true);
System.out.println(constructor1.newInstance());

Class<?> c2 = Class.forName("com.Singleton");
Constructor constructor2 = c2.getDeclaredConstructor(null);
constructor2.setAccessible(true);
System.out.println(constructor2.newInstance());
}
}

可以通过getClass()或者Class.forName()获取class对象,然后通过无参构造创建对象。

1
2
3
com.Singleton@1b6d3586
com.Singleton@4554617c
com.Singleton@74a14482

反序列化破解

如果单例类实现了Serializable接口进行序列化,可以通过反序列化破解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton implements Serializable{

private static volatile Singleton instance;

private Singleton(){}

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

}

先将对象序列化到文件,然后从文件中读取对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {

public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton instance = Singleton.getInstance();
System.out.println(instance);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/test.txt"));
objectOutputStream.writeObject(instance);

ObjectInputStream ous = new ObjectInputStream(new FileInputStream("D:/test.txt"));
Object o = ous.readObject();
System.out.println(o);
}
}

打印结果:

1
2
com.Singleton@1b6d3586
com.Singleton@312b1dae

解决办法

反射破解是通过调用构造方法进行实例化,只需要在构造方法中跑出异常即可。

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

private static volatile Singleton instance;

private Singleton(){
if (instance != null){
throw new RuntimeException();
}
}

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

}

反序列化方式可以通过增加readResolve()方法解决,这个方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要再创建新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton implements Serializable{

private static volatile Singleton instance;

private Singleton(){}

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

使用第六种枚举类型实现单例模式,可以避免反射和序列化破解。

  • jdk在反射的底层代码中限制了枚举不能通过newInstance()方法获取对象,里面判断类型是否是枚举,如果是直接抛出异常。
  • 枚举类型在序列化时是将枚举值的name存放到文件中的,反序列化时通过调用valueOf()方法获取对象。