单例介绍
单例模式是一种较基础的设计模式,他的设计思想是保证一个类的实例在内存中最多只有一个实例,可以大大节省内存开销,常见于系统配置,文件管理器,任务管理器,打印机等。
单例实现方式
饿汉式单例
懒汉式单例
饿汉式单例
系统初始化时就创建实例对象,通过静态方法创建
public class Singleton1 { //1.私有化构造器,使改类无法通过构造函数创建新实例 private Singleton1(){ } //2.系统初始化的时候创建静态对象,实现单例 private static Singleton1 singleton1 = new Singleton1(); //3.向外提供公共方法获取该单例 public static Singleton1 getSingleton1(){ return singleton1; }}
- 私有化构造器
- 创建静态对象,系统加载时就创建该实例
- 提供对外获取该实例的公共方法
懒汉式单例
用到对象的时候才实例化对象,如果改对象一直未被使用,则系统中一直没有改对象的实例存在,安全的懒汉式单例是通过“双重锁”实现
public class Singleton { //1.私有化构造器 private Singleton(){ //a } //2.提供唯一的对象,并声明为volatile关键字,系统初始化时不会创建该实例 private volatile static Singleton singleton = null; //b //3.对外提供静态方法 public static Singleton getSingleton(){ //c if(singleton == null){ //d //99%的情况下该实例已经创建,所以无需对着99%加锁,影响性能 //加锁对象是该类,使同一时间执行这一段代码的语句单个执行 synchronized (Singleton.class) { //e if(singleton==null){ //f singleton = new Singleton(); //g } } } return singleton; //h }}
- 私有构造函数
- 提供静态全局对象,但是私有,只能通过公共的对外方法才能获得
- 全局对象设置为volatile,避免指令重排序导致初始化失败(g段代码并不是原子操作,加上volatile,对他的写操作就会有一个内存屏障,避免指令重排序)
- 通过双重锁提供公共的对外获取该实例的方法
问题1:为什么要枷锁?
加锁的目的是为了避免并发的情况下,同时执行g段代码的时候会创建多个实例对象,破坏单例的思想。
问题2:为什么要用双重锁而不直接在if前面加锁?
双重锁实现,即先判断该对象是否为null,为null的时候在加锁,大大减少了加锁的颗粒度(因为99%的情况下该实例已经创建,既然是单例模式调用的次数会很高),系统执行效率更高效。
问题3:为什么要b段代码要加volatile关键字?
首先g代码不是原子操作,int a;a=1这都是原子操作,不会出现指令的重排序,但是g执行的时候要变成下面三个命令执行,而且是乱序的,因为操作系统会对指令重排序优化
1.为new出来的对象开辟空间
2.初始化,执行构造函数里面的代码片段
3.完成singletion引用赋值操作,将其指向开辟的内存空间
有可能先执行123,也有可能是132
如果并发两个线程同时执行e段代码,由于e段代码加锁,先获得锁的线程执行完efg代码后释放锁,另一个线程拿到锁后一次执行efg,不会出现问题
如果并发两个线程一个执行了g,一个执行了d,而且执行g的线程指令重排序了执行顺序是132,执行倒3的时候,另一个线程判断singletion是否为空,发现不为空直接返回空的对象,会出现报错的情况,所以要加volatile关键字,volatile关键字会禁止指令的重排序
单例安全
以上两种情况实现的单例,严格意义上来说还是不安全,因为可以通过反射破坏,序列话破坏单例,最安全的情况下是使用枚举实现单例
反射破坏单例
单例的重要实现之一是私有化构造函数,使外部代码不能通过new实现新的实例对象,但是反射可以绕过私有函数,代码如下:
public class Test { public static void main(String[] args) throws Exception{ Singleton s1 = Singleton.getInstance(); Constructorconstructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton s2 = constructor.newInstance(); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); }}
671631440输出结果为:
935563443
发现两者的hashcode不一样,说明不是同一个对象,单例失败
解决方法
在构造方法内判断singleton是否为null,不为null说明已经创建成功,直接返回已有的实例,如果不为null,则调用getInstance方法生成实例对象。
序列化破坏单例
该场景前提是Singleton 实现了Serializable
public void test() throws IOException, ClassNotFoundException { Singleton s1= null; Singleton s = Singleton.getInstance(); FileOutputStream fos = new FileOutputStream("Singleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("Singleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (Singleton)ois.readObject(); System.out.println(s==s1); }
False输出结果:
发现不是同一个对象,单例失败
解决方法
在Singleton类中加入readResolve()
private Object readResolve(){ System.out.println("read resolve"); return singleton; }
在jdk中ObjectInputStream的类中有readUnshared()方法,上面详细解释了原因。我简单描述一下,那就是如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”,然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。
枚举实现单例
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; }}
原理使用枚举类实现单例模式,在对枚举类进行序列化时,还不需要添加readRsolve方法就可以避免单例模式被破坏。
反编译后源码
public final class EnumSingleton extends Enum< EnumSingleton> { public static final EnumSingleton ENUMSINGLETON; public static EnumSingleton[] values(); public static EnumSingleton valueOf(String s); static {};}
1. 反射安全由代码可以看出实际编译后的代码时static对象,类似饿汉式单例,在系统初始化加载的时候就完成实例化操作,且保证线程安全
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { EnumSingleton singleton1=EnumSingleton.INSTANCE; EnumSingleton singleton2=EnumSingleton.INSTANCE; System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2)); Constructorconstructor= null; constructor = EnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); EnumSingleton singleton3= null; singleton3 = constructor.newInstance(); System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3)); }}
Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()结果报异常:
at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:20) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) 正常情况下,实例化两个实例是否相同:true
然后debug模式,可以发现是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性
public abstract class Enum> implements Comparable , Serializable { private final String name; public final String name() { return name; } private final int ordinal; public final int ordinal() { return ordinal; } protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } //余下省略
枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:
然后咱们看运行结果:
正常情况下,实例化两个实例是否相同:trueException in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:25) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
@CallerSensitive继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,只是在执行第17行(我没有复制import等包,所以行号少于我自己运行的代码)时候抛出异常,说是不能够反射,我们看下Constructor类的newInstance方法源码:
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
2. 序列化安全请看第12行源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同
枚举单例通过序列化创建可以创建成功,通过反射生成第二个的时候会失败。
参考链接