4种单例模式的写法,如何破解?如何防守?

4种单例模式

  1. 静态常亮字段
  2. DCL+volatile关键字
  3. 静态内部类
  4. 枚举类

打破单例模式:

  1. 反射
  2. 类加载器

第一种:静态常亮字段


/**
 * 1、静态常量
 * 步骤:1、创建一个类;2、构造器私有化;3、定义一个自身类型的静态常量字段并new出来对象;4、对该字段提供一个getter。
 * 优点:简单、实用
 * 缺点:一加载就被实例化了对象
 * 多线程安全性:高
 *
 * 有人说:只要加载了该类,即便没有调用getter的时候,类的对象还是被创建出来了,如果一直不调用getter方法,则创建出来的对象就是无用的
 * 有人回:你不用该对象你干嘛加载该类?
 */
public class S1 {
    
    
    private static final S1 INSTANCE = new S1();

    public static S1 getInstance() {
    
    
        return INSTANCE;
    }

    private S1() {
    
    
        System.out.println("S1()");
//        if (null != INSTANCE) {
    
    
//            throw new RuntimeException("不允许重复实例化该类的对象");
//        }
    }
}

第二种:DCL+volatile关键字

/**
 * 2、DCL + volatile关键字
 * 步骤:1、创建一个类;2、构造器私有化;3、定义一个自身类型的静态的volatile修饰的字段(不直接new);4、提供一个的getter方法,检查-加锁-检查-new,最后返回对象
 * 优点:解决了6中的在极少数情况下因指令重排导致getter方法返回null的问题
 * 缺点:
 * 多线程安全性:安全
 */
public class S2 {
    
    
    private static volatile S2 INSTANCE; // 提问点: volatile是必须的? 是必须的,防止cpu指令重排导致其他现成获取到未初始化完毕的对象
    private S2() {
    
    
        System.out.println("S2()");
//        if (null != INSTANCE) {
    
    
//            throw new RuntimeException("不允许重复实例化该类的对象");
//        }
    }

    public static S2 getInstance() {
    
    
        if (null == INSTANCE) {
    
     // 提问点:这次有必要判断吗?为了提升多线程判断效率
            synchronized(S2.class) {
    
    
                if (null == INSTANCE) {
    
     // 提问点:这次有必要判断吗?为了避免重复创建
                    INSTANCE = new S2();
                }
            }
        }
        return INSTANCE;
    }
}

第三种:静态内部类

/**
 * 3、静态内部类方式,跟之前的思路大不同
 * 步骤:1、创建一个类;2、构造器私有化;3、定义一个静态内部类,并在里面定义一个外部类类型的静态字段,同时new一个对象赋值给该字段;4、提供一个的getter方法,返回静态内部类的静态字段
 * 优点:不用复杂的双检查锁,jvm保证了多线程安全,保证了内部类只被加载一次,同时也实现了懒加载
 * 缺点:内部类增加了复杂度
 * 多线程安全性:安全
 */
public class S3 {
    
    
    private S3() {
    
    
        System.out.println("S3()");
//        if (null != Holder.INSTANCE) {
    
    
//            throw new RuntimeException("不允许重复实例化该类的对象");
//        }
    }

    private static class Holder {
    
    
        private static final S3 INSTANCE = new S3();
    }
    public static S3 getInstance() {
    
    
        return Holder.INSTANCE;
    }
}

第四种:枚举类

/**
 * 4、枚举单例,Java的作者之一推荐的绝对安全的单例模式的写法
 * 步骤:1、创建一个枚举(而不是类);2、不用构造器私有化;3、定义一个枚举的字段INSTANCE
 * 优点:在jvm层面保证是单例,可以防止反射(反序列化)创建对象
 * 缺点:给人看起来比较奇怪,不是常规的写法
 * 多线程安全性:安全
 */
public enum S4 {
    
    
    INSTANCE;
    private S4() {
    
    
        System.out.println("S4()");
    }
    public static S4 getInstance() {
    
    
        return INSTANCE;
    }
}

验证以上几种单例模式是否线程安全?

/**
 * 验证单例模式是否线程安全
 */
public class Verify {
    
    

    public static void main(String[] args) throws Exception {
    
    
        doVerify(S1::getInstance);
        doVerify(S2::getInstance);
//        doVerify(S2_1::getInstance);
        doVerify(S3::getInstance);
        doVerify(S4::getInstance);
    }

    private static void doVerify(Supplier instanceSupplier) throws Exception {
    
    
        List<Object> list = new LinkedList();
        int threadCount = 100;
        CountDownLatch cdl = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
    
    
            new Thread(() -> {
    
    
                try {
    
    
                    Thread.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                Object o = instanceSupplier.get();
                synchronized (list) {
    
    
                    list.add(o);
                }
                cdl.countDown();
            }).start();
        }
        cdl.await(1, TimeUnit.SECONDS);

        // 验证
        String simpleName = list.stream().findFirst().get().getClass().getSimpleName();
        Set<Integer> collect = list.stream().map(o -> o.hashCode()).collect(Collectors.toSet());
        if (collect.size() == 1) {
    
    
            System.out.println(simpleName + ": 多线程安全");
        } else if (collect.size() > 1) {
    
    
            System.err.printf(simpleName + ": 多线程不安全, 发现了'%s'个对象: '%s'\n", collect.size(), collect);
        }
    }
}

输出结果如下:

S1()
S1: 多线程安全
S2()
S2: 多线程安全
S3()
S3: 多线程安全
S4()
S4: 多线程安全

写一种线程不安全的单例模式:

public class S2_1 {
    
    
    private static S2_1 INSTANCE;
    private S2_1() {
    
    
        System.out.println("S2_1()");
    }
    public static S2_1 getInstance() {
    
    
        if (null == INSTANCE) {
    
    
            INSTANCE = new S2_1();
        }
        return INSTANCE;
    }
}

有不一样的结果:

S1()
S1: 多线程安全
S2()
S2: 多线程安全
S2_1()
S2_1()
S2_1: 多线程不安全, 发现了'2'个对象: '[122883338, 666641942]'
S3()
S3: 多线程安全
S4()
S4: 多线程安全

发现多了“S2_1: 多线程不安全, 发现了’2’个对象: ‘[122883338, 666641942]’”这么个结果。

用反射如何破解单例模式?

单例模式是希望保证系统中最多只能存在一个对象,但每种方式有不同的保证

/**
 * 探索: 如何用反射打破单例模式
 * 结论:只有枚举的方式不能通过反射创建,其余的均可以
 * 如何防止?
 * 在构造函数种增加重复创建的判断,如果重复创建了,则抛出异常
 */
public class ReflectBroker {
    
    
    public static void main(String[] args) throws Exception {
    
    
        {
    
    
            // 第1种
            Constructor<?> c1 = S1.class.getDeclaredConstructor();
            c1.setAccessible(true);
            System.out.println("s1 = " + c1.newInstance());
            System.out.println("s1 = " + c1.newInstance());
        }
        {
    
    
            // 第2种
            Constructor<?> c2_5 = S2.class.getDeclaredConstructor();
            c2_5.setAccessible(true);
            System.out.println("s2 = " + c2_5.newInstance());
            System.out.println("s2 = " + c2_5.newInstance());
        }
        {
    
    
            // 第3种
            Constructor<?> c3 = S3.class.getDeclaredConstructor();
            c3.setAccessible(true);
            System.out.println("s3 = " + c3.newInstance());
            System.out.println("s3 = " + c3.newInstance());
        }
        {
    
    
            // 第4种. 只有枚举方式通过反射失败,因为jvm不允许反射创建枚举对象
            Constructor<?>[] constructors = S4.class.getDeclaredConstructors();
            System.out.println("constructors = " + constructors.length);
            System.out.println("constructors = " + constructors[0]); // S4(java.lang.String,int)
            Constructor<?> c4 = constructors[0];
//            Constructor<S4> c4 = S4.class.getDeclaredConstructor(); // 没有默认构造函数
            c4.setAccessible(true);
            // IllegalArgumentException: Cannot reflectively create enum objects
            System.out.println("s4 = " + c4.newInstance("0", 0));
            System.out.println("s4 = " + c4.newInstance("1", 1));
        }
    }
}

执行结果:

S1()
S1()
s1 = com.ziv.test.dp.singleton.S1@45ee12a7
S1()
s1 = com.ziv.test.dp.singleton.S1@330bedb4
S2()
s2 = com.ziv.test.dp.singleton.S2@2503dbd3
S2()
s2 = com.ziv.test.dp.singleton.S2@4b67cf4d
S3()
s3 = com.ziv.test.dp.singleton.S3@7ea987ac
S3()
s3 = com.ziv.test.dp.singleton.S3@12a3a380
constructors = 1
constructors = private com.ziv.test.dp.singleton.S4(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.ziv.test.dp.singleton.ReflectBroker.main(ReflectBroker.java:47)

从以上结果可以看出,4个单例模式中的前3个都被成功的创建出了两个对象,也就成功打破了单例模式的“单个对象”的保证,但第4个单例模式却失败了,异常错误提示:“无法反射式创建枚举对象”
结论:只有枚举的方式不能通过反射创建,其余的均可以
如何防止?
在构造函数种增加重复创建的判断,如果重复创建了,则抛出异常,执行结果如下:

S1()
S1()
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.ziv.test.dp.singleton.ReflectBroker.main(ReflectBroker.java:21)
Caused by: java.lang.RuntimeException: 不允许重复实例化该类的对象
	at com.ziv.test.dp.singleton.S1.<init>(S1.java:26)
	... 5 more

这就安全了吗?

《深入理解Java虚拟机》中说:对于任意一个类,都需要由加载的类加载器与这个类本身一同确定其在JVM中的唯一性,每个类加载器,都拥有一个独立的类名称空间。——7.4.1节。

所以我们接下来就用类加载器来破解单例模式

用类加载器如何破解单例模式?


/**
 * 探索: 如何用 ClassLoader 来打破单例模式
 * 结论: 成功打破了所有形式的单例模式,包括反射方式未成功的枚举形式也在类加载器的方式中被打破
 */
public class ClassLoadBroker {
    
    

    public static void main(String[] args) throws Exception {
    
    
        doBroker(S1.class);
        doBroker(S2.class);
        doBroker(S3.class);
        doBroker(S4.class);
    }

    private static void doBroker(Class<?> targetClass) throws Exception {
    
    
        String getInstanceMethodName = "getInstance";

        // 1. 加载
        Class<?> originClass = targetClass;

        ClassLoader classLoader = new MyClassLoader();
        Class<?> reflectClass = classLoader.loadClass(targetClass.getName());

        // 2. 初始化
        Method originMethod = originClass.getMethod(getInstanceMethodName);
        originMethod.setAccessible(true);
        Object originInstance = originMethod.invoke(null);
        System.out.println(" originInstance = " + originInstance + ", hashCode: " + originInstance.hashCode());

        Method reflectMethod = reflectClass.getMethod(getInstanceMethodName);
        reflectMethod.setAccessible(true);
        Object reflectInstance = reflectMethod.invoke(null);
        System.out.println("reflectInstance = " + reflectInstance + ", hashCode: " + reflectInstance.hashCode());

        // 3. 对比
        if (originInstance.hashCode() != reflectInstance.hashCode()) {
    
    
            System.out.printf("打破类(%s)的单例模式成功: 存在两个不同的实例\n", targetClass.getSimpleName());
        }
    }

    public static class MyClassLoader extends ClassLoader {
    
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
            // 打破双亲委派模型,直接自己创建一个class,而不再判断父的类加载器是否加载过
            String pathName = name.substring(Math.max(0, name.lastIndexOf('.') + 1)) + ".class";
            URL resource = getClass().getResource(pathName);
            if (null != resource) {
    
    
                try {
    
    
                    InputStream in = resource.openStream();
                    byte[] bytes = new byte[in.available()];
                    in.read(bytes);
                    Class<?> aClass = this.defineClass(name, bytes, 0, bytes.length);
//                    System.err.println("加载到的类: " + aClass + ", hashCode: " + aClass.hashCode());
                    return aClass;
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
            return super.loadClass(name, resolve);
        }

    }
}

结果如下:

S1()
 originInstance = com.ziv.test.dp.singleton.S1@2b193f2d, hashCode: 723074861
S1()
reflectInstance = com.ziv.test.dp.singleton.S1@4dc63996, hashCode: 1304836502
打破类(S1)的单例模式成功: 存在两个不同的实例
S2()
 originInstance = com.ziv.test.dp.singleton.S2@5ca881b5, hashCode: 1554547125
S2()
reflectInstance = com.ziv.test.dp.singleton.S2@4517d9a3, hashCode: 1159190947
打破类(S2)的单例模式成功: 存在两个不同的实例
S3()
 originInstance = com.ziv.test.dp.singleton.S3@5305068a, hashCode: 1392838282
S3()
reflectInstance = com.ziv.test.dp.singleton.S3@279f2327, hashCode: 664740647
打破类(S3)的单例模式成功: 存在两个不同的实例
S4()
 originInstance = INSTANCE, hashCode: 41359092
S4()
reflectInstance = INSTANCE, hashCode: 713338599
打破类(S4)的单例模式成功: 存在两个不同的实例

这个……如何防呢?

知道了原因,就自然知道如何防止。既然类加载器可以随意定义,那就将单例类将系统默认的那个类加载器做绑定,即在构造函数中判断类加载器是否与系统默认的类加载器相同,如果不同则抛出异常。

绑定类加载器

代码如下:

public class S1 {
    
    
    private static S1 INSTANCE = new S1_1();
    static {
    
    
        System.out.println("classLoader = " + S1.class.getClassLoader());
    }
    private S1() {
    
    
        System.out.println("S1()");
        if (null != INSTANCE) {
    
    
            throw new RuntimeException("不允许重复实例化该类的对象");
        }
        // 防止类加载器破解, 将类加载与JVM的应用类加载器做绑定
        String onlyName = "sun.misc.Launcher$AppClassLoader";
        String actuallyName = getClass().getClassLoader().getClass().getName();
        if (Objects.equals(onlyName, actuallyName) == false) {
    
    
            throw new RuntimeException("不允许其他类加载器加载该类: " + getClass().getSimpleName());
        }
    }

    public static S1 getInstance() {
    
    
        return INSTANCE;
    }
}

结果如下:

classLoader = sun.misc.Launcher$AppClassLoader@7f31245a
S1()
 originInstance = com.ziv.test.dp.singleton.S1@2b193f2d, hashCode: 723074861
classLoader = com.ziv.test.dp.singleton.ClassLoadBroker$MyClassLoader@330bedb4
S1()
Exception in thread "main" java.lang.ExceptionInInitializerError
	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.ziv.test.dp.singleton.ClassLoadBroker.doBroker(ClassLoadBroker.java:66)
	at com.ziv.test.dp.singleton.ClassLoadBroker.main(ClassLoadBroker.java:22)
Caused by: java.lang.RuntimeException: 不允许其他类加载器加载该类: S1
	at com.ziv.test.dp.singleton.S1_1.<init>(S1.java:33)
	at com.ziv.test.dp.singleton.S1_1.<clinit>(S1.java:21)
	... 6 more

可以看到第一次创建S1 的时候打印出了classLoader 与系统默认的类加载器是相同的,所以正常执行了构造函数。但第二次用自定义的类加载器去创建 S1 的时候,由于构造函数中检测到类加载器的名称与系统默认的类加载器名称不同,所以抛出了异常。

猜你喜欢

转载自blog.csdn.net/booynal/article/details/125472603