单例模式(Singleton Pattern),其定义如下:
Ensure a class has only one instance, and provide a global point of accessto it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
单例模式的优点
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。例如,通过缓存的Json字符串频繁创建对象。
单例模式的缺点
- 扩展困难。若要扩展,除了修改代码基本上没有第二种途径可以实现。
- 对测试不友好。由于单例基本不存在接口(规定接口,mock对象实现接口进行测试),因此在并行开发的环境中,无法通过mock的方式虚拟一格对象。
单例使用场景
- 要求生成唯一序列号的环境;
- 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
单例模式的注意事项
- 线程安全问题
- 对象复制的情况。在Java中,Class实现了Cloneable接口并实现了clone()方法,那么就可以复制对象,且对象复制的过程是通过内存直接复制的,不会调用类的构造函数。解决该问题的最好方法就是不要实现Cloneable接口。
单例模式的7种实现
1. 懒汉(线程不安全)
public class Singleton {private static Singleton instance;private Singleton (){} //私有构造函数public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}}
2. 懒汉式(线程安全)
public class Singleton {private static Singleton instance;private Singleton (){}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}}
每次调用getInstance()都需要获取锁,效率太低,加锁的目的只是保证创建instance实例的安全性,当instance实例创建之后完全没有必要再去获取锁了。
3. 饿汉式单例
public class Singleton {private static Singleton instance = new Singleton();private Singleton (){}public static Singleton getInstance() {return instance;}}
由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全。
但是不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。
4. 饿汉式单例(静态代码块实现)
public class Singleton {private static Singleton instance = null;static {instance = new Singleton();}private Singleton (){}public static Singleton getInstance() {return instance;}}
5. 双重校验锁
public 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;
}
}
这种写法在去除了方法上的同步锁,采用互斥锁去创建实例。假设同时有两个线程通过了第一次检查,进入到了互斥锁,线程A获取到这个锁,线程B则阻塞等待。线程A执行初始化对象后,释放锁。线程B获得锁,由于有二次检查,实例已初始化就不会去再次初始化对象。
此时singleton必须使用volatile修饰!!!
volatile关键字有两个作用:
- 保持线程可见性
- 禁止指令重排序
创建对象的过程有三个步骤:
- 半初始化,申请内存,值为默认值
- 设初始值
- 将INSTANCE对象指向分配的内存空间
如果有一线程A执行到 singleton``= new Singleton() 语句,发生了指令重排,步骤二和步骤三颠倒了:
- 半初始化,申请内存,值为默认值
- 将
singleton对象指向分配的内存空间(此时singleton非null) - 设初始值
先将singleton对象和内存空间建立联系,此时为默认值。线程B又继续进行,发现singleton非空,于是直接返回singleton,造成各种不可预知的后果。因此在DCL单例模式中,必须要在实例上volatile关键字。
6. 静态内部类
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类不会因为外部类的加载而加载,而是在主动使用的时候才会加载。因此,只有显示调用getInstance方法时,才会显式加载SingleHolder类,既达到了懒加载的效果,也实现了线程安全。
注意:静态内部类方式算是比较完美的实现了,既是线程安全的,又是懒加载,相对于双重校验锁实现,写法有比较简单。但是,由于Java存在反射、克隆和序列化等机制,导致这些实现都不在安全,可以创建出新的对象。
6.1 反射破坏单例模式
Singleton single = Singleton.getInstance();
Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
dc.setAccessible(true);
Singleton singleCopy = dc.newInstance();
//false,单例被破坏
System.out.println(singleCopy == single);
防止通过反射破坏单例模式(在构造方法中判断instance是否为空从而抛出异常):
private Singleton() {
synchronized (Singleton.class) {
if (instance != null) {
throw new RuntimeException("Not allow to initialize again");
}
}
}
运行报错:
刚才是通过getInstance()方法创建了instance实例,导致通过反射创建的时候,instance实例已经存在故而报异常。但是,如果直接通过反射创建两个对象呢?
public static void main(String[] args) throws Exception {
// Singleton single = Singleton.getInstance();
Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
dc.setAccessible(true);
Singleton single = dc.newInstance();
Singleton singleCopy = dc.newInstance();
//false,单例被破坏
System.out.println(singleCopy == single);
}
解决方法,在构造其中设置标志位:
static boolean initialized = false;
private Singleton() {
if (!initialized) {
initialized = true;
} else {
throw new RuntimeException("Not allow initialized again!");
}
}
直接通过反射创建单例,直接报错:
问题好像解决了,但是,如果通过反射直接修改_initialized_的值,那这个判断又将失效:
public static void main(String[] args) throws Exception {
// Singleton single = Singleton.getInstance();
Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
dc.setAccessible(true);
Singleton single = dc.newInstance();
//再次通过反射修改属性值
Field flag = Singleton.class.getDeclaredField("initialized");
flag.setAccessible(true);
flag.set(dc,false);
Singleton singleCopy = dc.newInstance();
//false,单例被破坏
System.out.println(singleCopy == single);
}
这样看来,标志位也行不通了。那么,还有没有更安全的办法呢?答案是有的,那就是使用枚举创建单例。
序列化破坏单例模式
public static void main(String[] args) throws Exception {
Singleton s = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(s);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(s);
System.out.println(deserialize);
System.out.println(s == deserialize);
}
com.bujian.designpatterns.Singleton@327471b5 com.bujian.designpatterns.Singleton@36f6e879 false
那怎么解决这种情况呢?答案是在Singleton类中添加以下方法:
private Object readResolve() {
return instance;
}
再次运行代码,结果如下:
com.bujian.designpatterns.Singleton@327471b5 com.bujian.designpatterns.Singleton@327471b5 true
序列化内部也是通过反射获取对象的。readResolve方法只是系统提供的一个钩子,用来替换反射得到的新对象的。也就是说,其实系列化还是产生了新对象的,只不过被readResolve方法提供的实例拦截了,没用上。
7. 枚举
public enum Singleton {
INSTANCE;
}
这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
为啥枚举实现被称为最好的单例实现?
前几种方式实现单例都有如下3个特点:
- 构造方法私有化
- 实例化的变量引用私有化
- 获取实例的方法共有
这些实现方式都有一个共同的问题:私有化构造器并不保险。因为它抵御不了反射攻击。
**
其中防止方法已经在上文中给出,但是并不完美。因为不管是在构造函数在被第二次调用的时候抛出异常,还是采用标志位的方式,都可以通过多次反射破坏掉单例。要想解决这个问题,还得从newInstance()方法中寻找答案:
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!this.override) {
Class<?> caller = Reflection.getCallerClass();
this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);
}
if ((this.clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
} else {
ConstructorAccessor ca = this.constructorAccessor;
if (ca == null) {
ca = this.acquireConstructorAccessor();
}
T inst = ca.newInstance(initargs);
return inst;
}
}
主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0。说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败,因此枚举类型对反射是绝对安全的。
综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:
- 反射安全
- 序列化/反序列化安全
- 写法简单
