单例模式

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效,下面会讨论。)

主要优点:
1、提供了对唯一实例的受控访问。
2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
3、允许可变数目的实例。

主要缺点:
1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

以下列出单例模式的几种写法及单例模式的漏洞解决方案:

饿汉式

类初始化时创建对象,不管需不需要实例对象,都会创建。不存在线程安全问题,因为实例是在类创建和初始化时创建,是由类加载器完成的,类加载器是线程安全的。

缺点,无法延时加载,没有使用就已经加载了

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

private static final Singleton mInstance = new Singleton();

private Singleton(){

}

public static Singleton getInstance(){
return mInstance;
}

}

懒汉式

优化了恶汉式无法延迟加载的问题。
缺点:存在同步问题,多线程并发的时候会失效,getInstance不同步。比如:一个线程在创建mInstance时,还未创建完成,另一个线程访问mInstance此时还是为空,又创建了一次。

对懒汉式的优化,主要是在线程安全方面,使用synchronized关键字或同步代码块修饰,使得同时只能有一个线程访问。但存在性能缺陷的,因为使用了synchronized关键字。

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

private static Singleton mInstance;

private Singleton(){

}

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

}

DCL双重检查锁

DCL双重检查锁,是对第二种方法性能缺陷的优化。

DCL双重检查锁仅在真正创建mInstance实例的时候加上了synchronized关键字。而且使用volatile关键字修饰,是为了禁止编译器对volatile变量重排序,并且保证volatile变量的读操作发生在写操作之后。

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 mInstance =null; //volatile关键字是为了禁止编译器对 volatile关键字修饰的变量进行重排序,并保证volatile变量的读操作发生在写操作之后

private Singleton(){

}

public static Singleton getInstance(){
if(mInstance == null){ //第一次检查
synchronized (Singleton.class){ //同步代码块
if(mInstance == null){ //第二次检查
mInstance = new Singleton();
}
}
}
return mInstance;
}

}

静态内部类

利用static final关键字的同步机制,初始化后就无法修改保证了线程安全。使用静态内部类的方式保证了延迟加载,不使用不会被加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton { //完成了懒汉式的延迟加载,同时static保证了线程安全。

private Singleton(){

}

public static Singleton getIntance(){
return SingletonHolder.mIntance;
}

private static class SingletonHolder{ //私有的,初始化的时候,没有调用getIntance方法则不会加载
private static final Singleton mIntance = new Singleton(); //static,final是jvm提供的同步机制,初始化后就无法修改了
}

}

枚举(推荐用法)

1.简洁
2.线程安全
3.可以防止反射注入,反序列化它也不会重新生成新的实例

所有的枚举类型隐性地继承自java.lang.Enum 。枚举实质上还是类!而每个枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final 修饰的。可以直接通过枚举类型名使用它们。

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

INSTANCE;

public void doSomething(){
System.out.println("do sth.");
}

}

单例模式存在的漏洞

1、 通过反射获取单例对象

我们观察反射获取单例的代码,发现它还是调用了私有的构造方法获取对象【声明为私有的构造方法就是为了不让类外直接new对象】。如果只让私有的构造器只能调用一次就可以避免反射。

2、 反序列化获得单例模式对象

传统的单例模式的另外一个问题是一旦你实现了serializable接口,他们就不再是单例的了,因为readObject()方法总是返回一个 新的实例对象,就像java中的构造器一样。如果定义了readResolve()则直接返回此方法指定的对象,而不需要再创建新的对象!

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
/**
* 序列化必须实现Serializable接口,否则序列化时会报错
*/
public class Singleton implements Serializable{

private static final long serialVersionUID = 1L;

private static Singleton sl;


private Singleton() {
//如果sl不为空即这不是第一次调用该构造器
if(sl != null)
throw new RuntimeException();
}

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

/**
* 反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!
* @return
*/
private Object readResolve() {
return getInstance();
}
}

参考资料

为什么说枚举是最好的Java单例实现方法?