简介
什么是Singleton类?你之前用过Singleton吗?
Singleton是一个类,在整个应用程序中只有一个实例,并提供一个getInstance()方法来访问单例实例。在JDK中有许多类是使用Singleton模式实现的,如java.lang.Runtime,它提供了getRuntime()方法来访问它并用于获得Java中的可用内存和总内存。单例模式优缺点:
- 优点:
- 提供了对唯一实例的受控访问;
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;
- 允许可变数目的实例;
- 缺点:
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难;
- 单例类的职责过重,在一定程度上违背了“单一职责原则”;
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
- 优点:
单例模式的五种实现
懒汉
- 1.1 懒汉,线程不安全
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
- 1.2 懒汉,线程安全
1
2
3
4
5
6
7
8
9
10public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
- 1.1 懒汉,线程不安全
饿汉
- 2.1 饿汉,基本型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
```
这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
- 2.2 饿汉,变种
```Java
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起来差别挺大,其实跟2.1方式差不多,都是在类初始化即实例化instance。
- 2.1 饿汉,基本型
静态内部类
1
2
3
4
5
6
7
8
9public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟2.1和2.2方式不同的是(很细微的差别):2.1和2.2方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比2.1和2.2种方式就显得很合理。
- 双重校验锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这个是1.2方式的升级版,俗称双重检查锁定,详细介绍请查看:http://www.ibm.com/developerworks/cn/java/j-dcl.html
在JDK1.5之后,双重检查锁定才能够正常达到单例效果。
- 枚举
1
2
3
4
5public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化和通过反射重新创建新的对象,可谓是很坚强的壁垒啊,这种方式在1.5中才加入的enum特性。
单例模式的安全性问题
懒汉式、饿汉式、内部类、双重锁、枚举这5种单例模式中,枚举最为特殊,由于是官方提供的一种模式,所以不能被破解,是十分安全的。其他四种在一定程度上会有一定安全问题。
关于懒汉式的单利破解:
- 用反射破解单例模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import java.lang.reflect.Constructor;
public class Client {
public static void main(String[] args) throws Exception {
Singleton s011 = Singleton.getInstance();
Singleton s012 = Singleton.getInstance();
//利用反射创建新对象
Class<Singleton> h1 = (Class<Singleton>) Class.forName("Singleton");//反射获取H1对象
Constructor c = h1.getDeclaredConstructor(null);//获取无参构造函数
c.setAccessible(true);//更改无参构造函数权限为公开
Singleton s013 = (Singleton) c.newInstance(null);//创建新对象
System.out.println(s011);
System.out.println(s012);
System.out.println(s013);
System.out.println(s011 == s012);
System.out.println(s011 == s013);
}
}
输出的结果:1
2
3
4
5Singleton@1bc4459
Singleton@1bc4459
Singleton@12b6651
true
false
- 用反序列化(需要实现Serializable)单例模式:
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
29import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Client {
public static void main(String[] args) throws Exception {
Singleton s011 = Singleton.getInstance();
Singleton s012 = Singleton.getInstance();
//利用反序列化创建新对象
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s011);
fos.close();
oos.close();
FileInputStream fis = new FileInputStream("d:/a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton s013 = (Singleton) ois.readObject();
fis.close();
ois.close();
System.out.println(s011);
System.out.println(s012);
System.out.println(s013);
System.out.println(s011 == s012);
System.out.println(s011 == s013);
}
}
输出的结果是:1
2
3
4
5Singleton@13bad12
Singleton@13bad12
Singleton@1a626f
true
false
以上知道了如果破解,那么如果避免,代码应该这样写:
1 | import java.io.ObjectStreamException; |
总结
有两个问题需要注意:
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每 个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
该问题修复的办法是:1
2
3
4
5
6
7private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
该问题修复的办法是:1
2
3
4
5
6
7
8
9public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
private Object readResolve() {
//主要在这里!!!
return INSTANCE;
}
}
参考:
[1]. 单例模式的实现 https://blog.csdn.net/liangxw1/article/details/51353654
[2]. 单例模式面试问题 https://blog.csdn.net/u012613251/article/details/79477525
[3]. 单例模式的破解 https://blog.csdn.net/a445849497/article/details/57531266
[4]. 单例模式的优缺点 https://blog.csdn.net/tayanxunhua/article/details/8250329
[5]. 枚举实现单例模式更好 https://blog.csdn.net/normallife/article/details/51152246