一、单例模式的经典实现方式
单例模式分为饿汉式(立即加载)和懒汉式(延迟加载),其中懒汉式又可以分为双重检查锁、静态内部类和枚举三种情况。;
单例模式使用不当,则会产生线程安全问题:
饿汉式不会产生线程安全问题,但是它一般不使用,因为他会浪费内存空间;
懒汉式会合理的使用内存空间,因为只有第一次被加载的时候才会真正的创建对象;但是这种方式存在线程安全问题
创建单例模式的三要素:私有静态成员变量(对象本身)、私有的构造函数、公共的获取静态变量方法
1、饿汉式
类加载的时候,JVM内部保证了整个过程的线程安全。类加载包括静态成员变量的初始化。
所以饿汉式在类加载时,就会加载静态变量,从而对静态属性进行初始化,这个操作是受JVM线程保护的,不会出现线程安全问题。
package com.lcl.galaxy.design.pattern.singleton; public class HungrySingleAnimal { private static HungrySingleAnimal singleAnimal = new HungrySingleAnimal(); private HungrySingleAnimal(){ } public static HungrySingleAnimal getSingleAnimal(){ return singleAnimal; } }
2、懒汉式
懒汉式的最优写法就是使用双重检查锁的方式实现,实现代码如下:
package com.lcl.galaxy.design.pattern.singleton; import java.io.Serializable; public class DoubleCheckSingleAnimal implements Serializable {private static volatile DoubleCheckSingleAnimal singleAnimal = null; private DoubleCheckSingleAnimal(){ } public static DoubleCheckSingleAnimal getSingleAnima (){ if(singleAnimal == null){ synchronized (DoubleCheckSingleAnimal.class){ if(singleAnimal == null){ singleAnimal = new DoubleCheckSingleAnimal(); } } } return singleAnimal; } private Object readResolve(){ return singleAnimal; } }
为什么要这么实现呢,我们可以一步步的分析
最简单的实现方式:
private static volatile LazySingleAnimal1 singleAnimal = null; private LazySingleAnimal1(){ } /** * 不安全 * @return */ public static LazySingleAnimal1 getSingleAnimal(){ if(singleAnimal == null){ singleAnimal = new LazySingleAnimal1(); } return singleAnimal; }
这种实现方式会存在线程安全问题:当存在并发时,如果对象还未创建,则都会执行创建对象语句。
针对上述问题的优化如下
public static synchronized LazySingleAnimal1 getSingleAnima2(){ if(singleAnimal == null){ singleAnimal = new LazySingleAnimal1(); } return singleAnimal; }
优化方式是将方法增加一个synchronized同步锁,这样的问题就在于,会非常影响单例的使用性能
针对上述内容进一步优化
public static LazySingleAnimal1 getSingleAnima3 (){ if(singleAnimal == null){ synchronized (LazySingleAnimal1.class){ singleAnimal = new LazySingleAnimal1(); } } return singleAnimal; }
这种实现,只有在对象为空的时候才加锁,但是仍然存在问题:即如果存在并发,对象为空时,都能绕过为空判断,虽然在创建对象语句的执行上有锁,但是仍然会以串行方式创建多个对象。
继续上述问题优化
public static LazySingleAnimal1 getSingleAnima4 (){ if(singleAnimal == null){ synchronized (LazySingleAnimal1.class){ if(singleAnimal == null){ singleAnimal = new LazySingleAnimal1(); } } } return singleAnimal; }
这种情况已经控制住了,只有一个线程可以进入第二层为空判断,且创建了对象,释放锁后,后续竞争锁成功的线程判断时,对象已不为空,那么就不会在创建对象;但是该种实现仍然存在问题:JVM会对代码进行指令重排序,因此可能存在一个对象虽然已经创建,但是还未赋值的情况下,就被其他线程所使用,进而导致错误产生。因此需要对私有属性加volatile修饰。
这里需要说明一个指令重排序和volatile关键字的作用:
对象的创建流程:1、new关键字开辟空间;2、对象空间初始化;3、将内存地址赋值给栈空间的变量进行保存
指令重排序:JIT即时编译器,会对指令做重排序,上述创建顺序有可能不是按照123执行的,而是按照132执行的,那么就有个问题,先将引用对象做了保存,但是没有对对象的空间做初始化,那么下一个线程拿到的引用就会有问题
并发编程的三大特性:1、原子性:狭义上指CPU指令的原子操作,广义上指字节码指令的原子性;2、有序性:CPU指令有序性;3、可见性:CPU工作内存种的数据存在多核之间的不可见问题
volatile作用:解决有序性问题(禁止指令重排);解决可见性问题(强制刷新告诉缓存到内存中,当多个CPU对同一个数据进行操作时,一旦有数据有写操作,那么必须先等它写完数据之后,写入主内存)
这里还有一个问题,就是单例模式有可能被破坏,被破坏主要有反射破坏和序列化破坏,再最优的单例模式中增加了一个readResolve方法,就是为了防止使用序列化破坏。
3、静态内部类
静态内部类,主要是利用了静态类加载受JVM保护,在调用公共的get方法时,会去访问静态内部类的私有属性,此使就会触发静态内部类的加载,那么就初始化了静态内部类中的私有属性,同时又因为静态类的加载受JVM保护,因此静态内部类也是一个很好的单例实现方式。
package com.lcl.galaxy.design.pattern.singleton; public class StaticInnerClass { private static class SingletonHandler{ private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass(); } private StaticInnerClass(){} public StaticInnerClass getStaticInnerClass(){ return SingletonHandler.STATIC_INNER_CLASS; } }
4、枚举
先上代码,具体原因后面说。
package com.lcl.galaxy.design.pattern.singleton; public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
二、破坏单例
1、反射攻击
使用反射获取对象的无参构造,然后使用构造函数的newInstance方法进行创建对象。
@Test public void reflectAttackTest() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { Constructor constructor = DoubleCheckSingleAnimal.class.getDeclaredConstructor(); constructor.setAccessible(true); DoubleCheckSingleAnimal a1 = (DoubleCheckSingleAnimal) constructor.newInstance(); DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) constructor.newInstance(); a1.setName("lcl"); a2.setName("lcl"); log.info("a1============={}==========",a1); log.info("a2============={}==========",a2); log.info("a1.equals(a2)============={}==========",a1.equals(a2)); }
2、序列化攻击
使用深拷贝的方式,创建两个对象。其实就是先使用序列化,将对象写到一个文件内,然后再将文件中的内容反序列化为对象。
@Test public void serializationAttackTest() throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("aaa")); DoubleCheckSingleAnimal a1 = DoubleCheckSingleAnimal.getSingleAnima(); oos.writeObject(a1); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("aaa")); DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) ois.readObject(); a1.setName("lcl"); a2.setName("lcl"); log.info("a1============={}==========",a1); log.info("a2============={}==========",a2); log.info("a1.equals(a2)============={}==========",a1.equals(a2)); }
三、攻击防御
1、反射攻击防御
对于反射攻击,private属性简直形同虚设,同时反射是基于对象的构造函数进行创建对象的,那么就可以在构造函数中加入判断,如果对象不为空,则抛出异常
package com.lcl.galaxy.design.pattern.singleton; public class StaticInnerClass { private static class SingletonHandler{ private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass(); } private StaticInnerClass(){} public StaticInnerClass getStaticInnerClass() throws Exception { if(SingletonHandler.STATIC_INNER_CLASS != null){ throw new Exception("单例被破坏"); } return SingletonHandler.STATIC_INNER_CLASS; } }
2、反序列化攻击防御
这里可以看一下序列化时调用的ois.readObject方法,在该方法的源码中,会判断是否存在readResolve方法,如果存在,则调用该方法进行处理,如果不存在,则反序列化对象,因此,在上面提到,双重检查锁的单例模式,需要加入readResolve方法,这样就可以避免通过序列化攻击单例的情况(上面代码示例的readResolve方法)。
package com.lcl.galaxy.design.pattern.singleton; import java.io.Serializable; public class DoubleCheckSingleAnimal implements Serializable { private static volatile DoubleCheckSingleAnimal singleAnimal = null; private DoubleCheckSingleAnimal() throws Exception { } public static DoubleCheckSingleAnimal getSingleAnima () throws Exception { if(singleAnimal == null){ synchronized (DoubleCheckSingleAnimal.class){ if(singleAnimal == null){ singleAnimal = new DoubleCheckSingleAnimal(); } } } return singleAnimal; } private Object readResolve(){ return singleAnimal; } }
3、枚举单例防御
JVM对枚举类做了特殊的处理,既保证了线程安全,又防止了序列化攻击,同时也防止了反射攻击。
public abstract class Singleton extends Enum
保证线程安全:查看枚举类编译后的文件(如上述代码)可以发现,枚举类是用static修饰的类;JVM在加载static类的时候,会保证线程安全
防止序列化攻击:JVM对枚举类做了特殊处理,在序列化时,只保存了属性名称和引用地址,当反序列化时,使用的是引用地址和名称进行处理,因此拿到的对象仍然是同一个对象。
防止反射攻击:枚举编译后的文件是抽象类,因此不能使用反射进行破坏。