How to write 4 singleton patterns, how to crack them? How to defend?

4 singleton modes

  1. Static always-on field
  2. DCL+volatile keyword
  3. static inner class
  4. enum class

Breaking the singleton pattern:

  1. reflection
  2. class loader

The first type: static always-on field


/**
 * 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("不允许重复实例化该类的对象");
//        }
    }
}

Second type: DCL+volatile keyword

/**
 * 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;
    }
}

The third type: static inner class

/**
 * 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;
    }
}

The fourth type: enumeration class

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

Verify whether the above singleton modes are thread-safe?

/**
 * 验证单例模式是否线程安全
 */
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);
        }
    }
}

The output is as follows:

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

Write a thread-unsafe singleton pattern:

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;
    }
}

There are different results:

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

More results like "S2_1: Multi-threading is not safe, '2' objects found: '[122883338, 666641942]'" were found.

How to break the singleton pattern using reflection?

The singleton mode hopes to ensure that there can be at most one object in the system, but each method has different guarantees.

/**
 * 探索: 如何用反射打破单例模式
 * 结论:只有枚举的方式不能通过反射创建,其余的均可以
 * 如何防止?
 * 在构造函数种增加重复创建的判断,如果重复创建了,则抛出异常
 */
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));
        }
    }
}

Results of the:

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)

It can be seen from the above results that the first three of the four singleton patterns have successfully created two objects, which successfully broke the "single object" guarantee of the singleton pattern, but the fourth singleton The mode failed, with an exception error message: "Unable to create an enumeration object reflectively."
Conclusion: Only enumeration methods cannot be created through reflection, and the rest can be
prevented. How to prevent it?
Add the judgment of repeated creation in the constructor. If it is created repeatedly, an exception will be thrown. The execution result is as follows:

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

Is this safe?

No

"In-depth Understanding of the Java Virtual Machine" says: For any class, the loaded class loader and the class itself need to determine its uniqueness in the JVM. Each class loader has an independent class name. space. ——Section 7.4.1.

So we will use the class loader to crack the singleton mode

How to break singleton pattern using class loader?


/**
 * 探索: 如何用 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);
        }

    }
}

The result is as follows:

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)的单例模式成功: 存在两个不同的实例

How to prevent this...?

If you know the cause, you will naturally know how to prevent it. Since the class loader can be defined at will, the singleton class is bound to the system default class loader, that is, in the constructor, it is judged whether the class loader is the same as the system default class loader, and if it is different, it throws abnormal.

Binding class loader

code show as below:

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;
    }
}

The result is as follows:

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

S1You can see that the classLoader printed out when it was first created is the same as the system's default class loader, so the constructor is executed normally. But when the custom class loader was used to create it for the second time S1, an exception was thrown because the name of the class loader detected in the constructor was different from the system default class loader name.

Guess you like

Origin blog.csdn.net/booynal/article/details/125472603