单例模式的几种形态
饿汉式
//饿汉式单例 public class Hungry { private Hungry(){ } private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } }
问题:
可能浪费内存(饿汉式一开始就将程序中所有的东西都加载好,但有些东西暂时用不到,就会浪费内存)
DCL懒汉式
//懒汉式单例 public class LazyMan { private LazyMan(){ System.out.println(Thread.currentThread().getName()+"完成"); } private volatile static LazyMan lazyMan; // 双重检测锁模式的懒汉式单例(DCL懒汉式) public static LazyMan getInstance(){ // 加锁(此锁保证只有这个类只有一个) if(lazyMan==null){ synchronized (LazyMan.class){ if(lazyMan==null){ /** * 此代码会经过的步骤 * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向这个空间 * 平时我们期望的运行路线是123,但实际的运行路线可能是132 * A线路没问题,可以正常通过 * 但如果还有一个B线路,因为此对象已经先指向了这个空间 * B线路就会认为该构造对象不为null,但此时lazyMan还没有完成构造,可能会出现问题 */ //在极端情况下此部分代码是有问题的,因为它不是原子性操作 lazyMan = new LazyMan(); } } } return lazyMan; } // 单线程下确实没问题 // 多线程并发的情况下是有问题的,会同时实例化多个单例 public static void main(String[] args) { for (int i = 0; i < 10; i++){ new Thread(()->{ LazyMan.getInstance(); }).start(); } } }
问题:
一般的懒汉模式在单线程下没问题,但没有加synchronized锁的情况下在多线程环境下会出现生成不止一个对象的情况。
懒汉式单例实现思想为需要用到的时候再对程序中的东西进行加载,不需要时不加载,避免了浪费内存
静态内部类(炫技)
//静态内部类 public class Holder { private Holder(){} public static Holder getInstance(){ return InnerClass.HOLDER; } public static class InnerClass{ private static final Holder HOLDER = new Holder(); } }
单例最重要的思想是构造器私有
破坏单例模式
通过反射破坏单例模式(反射使用类的构造方法)
//懒汉式单例 public class LazyMan { private LazyMan(){ System.out.println(Thread.currentThread().getName()+"完成"); } private volatile static LazyMan lazyMan; // 双重检测锁模式的懒汉式单例(DCL懒汉式) public static LazyMan getInstance(){ // 加锁(此锁保证只有这个类只有一个) if(lazyMan==null){ synchronized (LazyMan.class){ if(lazyMan==null){ /** * 此代码会经过的步骤 * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向这个空间 * 平时我们期望的运行路线是123,但实际的运行路线可能是132 * A线路没问题,可以正常通过 * 但如果还有一个B线路,因为此对象已经先指向了这个空间 * B线路就会认为该构造对象不为null,但此时lazyMan还没有完成构造,可能会出现问题 */ //在极端情况下此部分代码是有问题的,因为它不是原子性操作 lazyMan = new LazyMan(); } } } return lazyMan; } // 在反射面前所有的单例模式都是不安全的 public static void main(String[] args) throws Exception { LazyMan instance = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // Accessable属性是继承自AccessibleObject 类. 功能是启用或禁用安全检查。它不是让字段的访问权限变了,私有字段依然是私有字段,非本类对象依然是不能直接访问该字段的。它的作用是,不进行访问检查,当执行后面的操作的时候,JVM会直接忽略字段的访问控制符,直接进行操作。 declaredConstructor.setAccessible(true); LazyMan instance2 = declaredConstructor.newInstance(); // 此处获取的结果本该是一样的,因为他们是同一个单例模式,但实际上通过setAccessible绕过访问检查,通过declaredConstructor new了一个新的Instance,创建出了两个都指向单例模式的对象 /** * 得出结果为: * LazyMan@16d3586 * LazyMan@154617c * 得出结论:反射可以破坏单例模式 */ System.out.println(instance); System.out.println(instance2); } }
反射破坏单例模式的反制手段(构造方法加锁)
private LazyMan(){ // 反射破坏单例的反制手段 synchronized (LazyMan.class){ // 在此处判断类是否为空,为空正常创建,如果有值,则判定第二次创建是使用反射来破坏单例模式的 if(lazyMan!=null){ throw new RuntimeException("拦截反射破坏单例"); } } System.out.println(Thread.currentThread().getName()+"完成"); }
因为反射走了单例模式的无参构造器,所以可以在构造器中加把锁进行判断
通过反射破坏单例模式(不使用类的构造方式)
public static void main(String[] args) throws Exception { // LazyMan instance = LazyMan.getInstance(); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance = declaredConstructor.newInstance(); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(instance2);
反射破坏单例模式的反制手段(信号灯)
信号灯模式:
生产者生产,消费者等待,生产完成后通知消费。
消费者消费,生产者等待,消费者消费完成后通知生产
private static boolean wenying = false; private LazyMan(){ if(wenying == false){ wenying = true; }else { throw new RuntimeException("拦截反射破坏单例"); } } }
通过反射破坏单例模式(知道标志位的名)
public static void main(String[] args) throws Exception { // 假设知道标志位的name,在此处获取标志位的值 Field wenying = LazyMan.class.getDeclaredField("wenying"); // 再次通过setAccessible绕过访问检查 wenying.setAccessible(true); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); LazyMan instance = declaredConstructor.newInstance(); // 更改标志位的值 wenying.set(instance,false); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(instance2); }
如何才能保证单例模式不被破坏?
根据查看源码得知,如果反射传过来的是一个枚举常量,则会提示不能使用反射破坏枚举
举例枚举是否可以被反编译
通过target查看EnumSingle源码为
public enum EnumSingle { INSTANCE; private EnumSingle() { } public EnumSingle getInstance() { return INSTANCE; } }
那么此时可以使用之前用过的,反射使用类的构造方法来破坏枚举:
//enum是一个什么? 本身也是一个class类 public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }
运行结果为:Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>(),并不是源码中的Cannot reflectively create enum objects。翻译为EnumSingle中没有无参构造。
但是在target中这个类确实是有无参构造的,进入该类的文件夹地址,使用cmd通过javap进行反编译,能得到枚举类其实就是java类继承了枚举的结论,但此时编译出来,依然有无参构造方法,实际运行时报错明明已经说明了此类没有无参构造。
使用jad反编译工具再次进行编译,将该类转换为java类,此时原本类中的无参构造变为了构造方法(String s,int i)的方法
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://kpdus.tripod.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumSingle.java public final class EnumSingle extends Enum { public static EnumSingle[] values() { return (EnumSingle[])$VALUES.clone(); } public static EnumSingle valueOf(String name) { return (EnumSingle)Enum.valueOf(EnumSingle, name); } private EnumSingle(String s, int i) { super(s, i); } public EnumSingle getInstance() { return INSTANCE; } public static final EnumSingle INSTANCE; private static final EnumSingle $VALUES[]; static { INSTANCE = new EnumSingle("INSTANCE", 0); $VALUES = (new EnumSingle[] { INSTANCE }); } }
再次回到程序中进行试验
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
最终运行结果为Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects,运行正确
得出结论:枚举类型参数无法被反射破坏。
个人问题:
不清楚源码是否有方法修改,如果可以,将源码中限制枚举类型的代码去掉,这种方式也被破解了