单例模式(Singleton)
在一些系统中,出于节省内存资源、保证数据内容一致性等考虑,对某一些类要求只能创建一个实例,这就是所谓的单例模式。
定义与特点
定义:一个类只有一个实例,且该类能自行创建这个实例。
特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
优缺点
优点:
- 在内存中只有一个实例,减少了内存的开销;
- 避免了对资源的多重占用;
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点:
- 单例模式没有抽象层,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现,违背了开闭原则。
- 单例模式功能代码通常写在一个类中,若设计不合理,很容易违背单一职责原则。
应用场景
- 需要频繁创建对象
- 某些类实例化占用资源过多或实例化耗时较长且经常使用
- 某些类需要频繁实例化,而创建的对象有频繁被销毁
- 频繁访问数据库或文件的对象
- 控制硬件级别的操作,或者从系统上来讲应该是单一控制逻辑的操作,若有多个实例会破坏系统正常运行
- 对象需要被广泛共享
结构与实现
结构
单例类:包含一个实例且能自行创建这个实例的类
访问类:使用实例的类
实现
懒汉模式
加载时不生成单例,第一次调用getlnstance 方法时才去创建这个单例。
public class Singleton {
private static Singleton singleton = null;
//构造方法私有
private Singletion(){}
public static Singleton getlnstance() {
//如果没有实例化,就实例化一个,然后返回
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
不过显然这段代码是线程不安全的,那么以下是线程安全的懒汉模式。
public class Singleton {
private static volatile Singleton singleton = null;
private Singletion(){}
public static synchronized Singleton getlnstance() {
//如果没有实例化,就实例化一个,然后返回
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
线程安全的懒汉模式使用volatile 和 synchronized保证了线程的安全,但是每次调用getlnstance方法都需要同步,会影响性能且消耗更多的资源。
饿汉模式
当类加载的时候就创建一个单例。
public class Singleton {
private static Singleton singleton = new Singletion();
private Singletion(){}
public static Singleton getlnstance() {
return singleton;
}
}
饿汉模式不存在性能和线程安全问题(JVM加载类的时候是单线程的),但是因为加载时就创建,因此可能会产生很多无用的实例。
双重校验
双重校验和懒汉模式类似,也是一种延迟加载,但是解决了线程安全懒汉模式的效率问题。
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
缩小了synchronized范围,避免大范围加锁造成的效率问题。
双重校验,第一重校验防止实例化后调用该方法代码加锁影响效率,第二重校验防止有线程A实例化时线程B等待锁造成线程B获取锁后再次实例化对象。
在new对象的时候,有三个步骤:分配内存空间,初始化对象,将内存地址赋值给变量;在这三个步骤中,步骤2和3有可能会在操作上进行重排序,在重排序的情况下,还没有初始化对象,先将内存地址赋值给了变量,当线程B进入时,发现变量不为null,就会直接返回这个实例,然而此时可能拿到的是还没有初始化完成的对象。所以双重锁需要volatile关键字防止重排序。
静态内部类
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
Java静态内部类的加载的时候不会被加载,使用的时候才会进行加载。而使用到的时候类加载又是线程安全的,可以完美解决上述的问题。
单元素枚举类
public enum Singleton {
INSTANCE;
public void doSomething(){
}
}
调用时:
Singleton.INSTANCE.doSomething();
枚举功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化。单元素的枚举类型被《Effective Java》作者认为是实现Singleton的最佳方法。
问题
类实例化的方式不只有new关键字,还有反射、反序列化和克隆。
除了单元素枚举类单例天生免疫反射、反序列化和克隆外,其他实现方法均会被反射、反序列化和克隆破坏单实例。
反射
破坏示例
Singleton singleton = Singleton.getInstance();
Class<?> singletonClass = singleton.getClass();
Constructor<?> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);//取消访问检查
Singleton newSingleton = (Singleton) singletonConstructor.newInstance();
任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,也可以获得类的构造器(就算是私有也没用),从而构造一个新的对象。
解决
我们可以在单例类中添加一个标志位来防止其多次实例化,以饿汉模式为例:
private static boolean flag = false;
private static Singleton singleton = new Singleton();
private Singleton(){
if (!flag){
flag = true;
}else {
throw new RuntimeException("不可多次实例化");
}
}
public static Singleton getInstance(){
return singleton;
}
但这其实没有什么意义,因为我们仍然可以利用反射机制修改标识位来实现多次实例化。
Singleton singleton = Singleton.getInstance();
Class<?> singletonClass = singleton.getClass();
//修改标志位
Field field = singletonClass.getDeclaredField("flag");
field.setAccessible(true);
field.set(singleton, false);
//再次实例化
Constructor<?> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);
Singleton newSingleton = (Singleton) singletonConstructor.newInstance();
反序列化
破坏示例
Singleton singleton = Singleton.getInstance();
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singletonTest"));
oos.writeObject(singleton);
oos.close();
//反序列化
File file = new File("singletonTest");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newSingleton = (Singleton) ois.readObject();
解决
作为解决,我们可以在单例类中添加一个新方法readResolve:
private Object readResolve() {
return singleton;
}
这个readResolve方法只做了一个简单的事情,反序列化的时候,首先检查这个类有没有readResolve方法,若有,则返回readResolve指定的对象,若没有,才把反序列化的结果返回。
克隆
破坏示例
Singleton singleton = Singleton.getInstance();
Singleton newSingleton = (Singleton) singleton.clone();
解决
和反实例化的解决方法类似,在重写的clone方法中返回单例类自身:
@Override
public Object clone() throws CloneNotSupportedException {
return singleton;
}
单元素枚举类单例为什么免疫这些问题
反射
看一下Constructor类源码:
public T newInstance(Object ... initargs){
…………
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
…………
}
底层源码直接禁止了通过反射机制来创建一个枚举对象。
反序列化
所有枚举类都是Enum类的子类,枚举中可以声明多个对象,每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量时的名字(比如INSTANCE),ordinal就是声明的顺序(比如若INSTANCE是第一个声明的,则为0)。
在java规范中对枚举类型的序列化和反序列化做了特殊规定,在序列化和反序列化期间,任何特定于类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法都会被忽略。 同样,任何serialPersistentFields或serialVersionUID字段声明也会被忽略,所有枚举类型的fixedserialVersionUID都是0L。(也就是说枚举类型序列化反序列化机制与其他类型的不一样)。序列化时,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。
枚举在序列化和反序列化过程中不会创建新的对象,而是根据name拿到原有的对象,因此保证了单实例。
克隆
所有枚举类都是Enum类的子类,在Enum中clone方法被定义成protected final,且具体实现就只是抛出CloneNotSupportedException异常。
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
因此枚举类无法重写并实现clone方法,所以无法通过克隆创建新实例。