单例模式是java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式设计到一个单一的类,该类负责创建自己的对象,同时保证只有单个对象被创建,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
概述
意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当你想控制实例数目,节省系统资源的时候。
实现
- 构造函数私有。
- 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
优点
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;
- 避免对资源的多重占用。
缺点
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
代码示例
创建一个Singleton类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class 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) {
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()
方法获取对象。