单例模式是一种创建型模式,用于创建对象,它的特点在于该类全局只有一个对象实例,并且对外提供一个公共入口来访问这个实例。
常见实现方式
1、饿汉式的单例模式实现【可用,但不推荐】
public class MySingleton {
//私有的静态成员变量,在类加载时直接创建对象
private static final MySingleton instance = new MySingleton();
//私有的构造方法,防止外部调用来创建对象
private MySingleton() {
}
//对外访问单例对象的入口
public static MySingleton getInstance() {
return instance;
}
}
这种方式属于饿汉式,也即是说它比较着急,在类加载过程中就实例化了单例对象,而不是实际用到的时候再创建(比如调用getInstance方法时)。
这种方式虽然是线程安全的,但问题在于不是真正需要的时候才创建对象,没有起到懒加载的作用,造成了不必要的内存占用。
2、懒汉式的单例模式实现【线程不安全,不可用】
public class MySingleton {
private static MySingleton instance;
private MySingleton() {
}
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
现在对象的创建转移到了 getInstance 方法里,实现了懒加载,但是很明显,这种写法是线程不安全的,对象创建完成之前,其他线程读到了instance == null,也会去创建,就不符合单例原则了。
3、双检锁 + volatile实现【推荐】
public class MySingleton {
private static volatile MySingleton instance;
private MySingleton() {
}
public static MySingleton getInstance() {
if (instance == null) {
//synchronized锁之所以加在这里而不是最外层,是为了减少不必要的同步,提高效率
synchronized (MySingleton.class) {
//这里第二次判空,正是因为加锁范围较小,第一次判空并没有同步
if (instance == null) {
/*
注意这条语句并非原子操作,内部有可能发生指令重排,导致对象未初始化时先将地址暴露给了instance引用,
其他线程判断到instance不为null,则会直接使用该对象,出现线程安全问题。
所以instance属性必须使用volatile关键字修饰,从而禁止相关的指令重排。
*/
instance = new MySingleton();
}
}
}
return instance;
}
}
这种实现方式是懒汉式的改进实现,懒加载的同时保证了线程安全。
4、静态内部类实现【推荐】
public class MySingleton {
private MySingleton() {
}
private static class InstanceHolder {
static final MySingleton instance = new MySingleton();
}
public static MySingleton getInstance() {
return InstanceHolder.instance;
}
}
静态内部类 InstanceHolder 只有在被使用到的时候才会进行类加载(也就是调用getInstance方法时),然后创建instance对象,从而实现了懒加载。同时单例对象跟随类的初始化而创建,保证了线程安全。
5、枚举实现【推荐】
public enum MySingleton {
INSTANCE;
public static void main(String[] args) {
MySingleton mySingleton = MySingleton.INSTANCE;
}
}
枚举是最简单且安全的实现方式,它不仅避免了线程安全问题,而且还能天然地防御反射和反序列化的方式对单例模式的破坏。
反射对单例模式的破坏和防御
反射机制可以直接调用类的构造器来创建对象,而不需要调用 MySingleton 的 getInstance方法,这样每次反射调用 newInstance 方法都会产生的新的对象,从而破坏掉单例模式。
上述的几种实现方式中,只有枚举方式的实现,能够天然地防御反射破坏,因为JDK代码中通过反射机制,调用构造器的newInstance方法时,会检查类型是否为枚举类型,如果是则会抛异常。也就是说java中不允许通过反射的方式构造枚举实例。
而对于其他方式,我们必须从构造方法入手,通过编码的方式防止反射破坏。
1、饿汉式和静态内部类的方式
这两种方式的共同点在于,都是在类的初始化过程中,实例化了单例对象;而通过反射api,比如 newInstance 方法创建对象时,就会先触发类加载和初始化,所以对于这两种方式,我们在构造函数中添加实例是否为空的判断,即可防止单例模式被反射破坏。
饿汉式:
public class MySingleton {
private static final MySingleton instance = new MySingleton();
//此处加个判断
private MySingleton() {
if (instance != null) {
throw new RuntimeException("单例对象已经存在");
}
}
public static MySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("org.example.thread.MySingleton");
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
MySingleton mySingleton1 = (MySingleton) constructor.newInstance();
MySingleton mySingleton2 = (MySingleton) constructor.newInstance();
System.out.println(mySingleton1);
System.out.println(mySingleton2);
}
}
运行结果:
静态内部类方式:
public class MySingleton {
//此处加个判断
private MySingleton() {
if(InstanceHolder.instance != null){
throw new RuntimeException("单例对象已经存在");
}
}
private static class InstanceHolder {
static final MySingleton instance = new MySingleton();
}
public static MySingleton getInstance() {
return InstanceHolder.instance;
}
}
通过反射调用构造方法时,会先触发内部类InstanceHolder的类加载和初始化,从而创建单例对象,所以这里的条件恒为true,然后就会报错。而正常的使用getInstance方法,仅会触发一次构造函数的调用,不会抛异常。
2、双检锁方式
对于该方式,修改构造函数并不能完全防止单例被反射所破坏,比如:
private MySingleton() {
if(instance != null){
throw new RuntimeException("单例对象已经存在");
}
}
这里 instance != null 的前提是调用过 getInstance 方法生成了单例对象,但是 如果在调用getInstance之前,直接连续地使用反射来创建对象,则对象仍然不是单例的。
反序列化对单例模式的破坏和防御
以静态内部类的实现方式为例,反序列化对单例模式的破坏如下:
public class MySingleton implements Serializable {
private MySingleton() {
}
private static class InstanceHolder {
static final MySingleton instance = new MySingleton();
}
public static MySingleton getInstance() {
return InstanceHolder.instance;
}
public static void main(String[] args) throws Exception {
MySingleton mySingleton1 = MySingleton.getInstance();
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("MySingleton.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(mySingleton1);
//反序列化
FileInputStream fileInputStream = new FileInputStream("MySingleton.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
MySingleton mySingleton2 = (MySingleton) objectInputStream.readObject();
System.out.println(mySingleton1);
System.out.println(mySingleton2);
}
}
执行mai方法,结果如下:
反序列化的实质还是通过反射调用了构造器的newInstance方法,创建出了新的对象。不过反序列化的破坏也是可以防御的,在readObject方法中,会检查并调用类中定义的 readResolve方法,如果该方法返回了对象,那么反序列化就会使用readResolve返回的对象作为结果。所以我们需要做的就是在类中添加一个readResolve方法。如下:
private Object readResolve() {
return InstanceHolder.instance;
}
再次运行会发现,反序列化得到的对象为原对象。
对于枚举方式实现的单例,不用担心反序列化带来的破坏,因为在java的反序列化中,对枚举类型的反序列化,不会创建新的实例。
clone()方法对单例模式的破坏和防御
实现了Cloneable接口的单例类,调用其对象的clone()方法,也会创造出新的对象,破坏单例模式(枚举除外,它不允许被clone)。
可以通过重写clone()方法直接返回已有的实例对象来解决。
总结:
饿汉式、双检锁式、静态内部类、枚举类型都是可用的单例模式实现方案,可以根据需求不同进行选择,当然最推荐的还是枚举方式的实现,不仅实现简单,而且天然免疫反射、反序列化、clone()等任何形式的破坏,非常安全。