重学Java之单例模式--DCL懒汉式

接前篇 重学Java之单例模式–懒汉式_南国樗里疾的博客-CSDN博客
https://blog.csdn.net/weixin_44021334/article/details/114880800 ,
说到懒汉式多线程不安全,
改进办法是 双重锁模式,也就是 双重锁定(Double-Check Locking) ,简称 DCL 。

1.简单加锁

对外提供获取实例的方法加上锁,

package single;

public class DCLLazyMan {

    private static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        System.out.println(Thread.currentThread().getName() + ", " + toString());
    }
    
    public synchronized static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            if (dclLazyMan == null) {
                dclLazyMan = new DCLLazyMan();
            }
        }
        return dclLazyMan;
    }
}

这样是可以实现了,但是每次调用 getInstance() 都受到同步锁的影响,效率低。
改进下,

2.双重锁

双重锁代码示例,

package single;

public class DCLLazyMan {

    private static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) { // 注释1
            synchronized (DCLLazyMan.class){ //注释 2
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args){
        //多线程
        for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }
    }
}

注释1 :将 synchronized 关键字从方法声明中去除,把它加入到方法体当中,效果是和直接在方法上加 synchronized 是一样的。在实例还未创建时才会运行到注释2处 ,如果实例已创建,不会执行到 注释 2 处,不受同步锁影响,效率较好。

注释 2 :实例未创建,需要创建实例,加锁保证只有一个线程操作。

运行结果:

Thread-0, single.DCLLazyMan@63b4a181

3.双重检测锁 + 原子性操作

2中创建实例的代码 dclLazyMan = new DCLLazyMan() ,不是原子性操作,
它包含3个步骤:

  • 1.分配内存空间;
  • 2.执行构造方法,初始化对象;
  • 3.把对象指向这个内存空间。

我们期望的执行顺序是 123 ,但是实际执行的情况可能是 132 ,涉及指令重排, CPU 是支持的,单线程情况下没问题。

多线程的就不行了,假设 A 线程执行到 13 ,B 线程执行,因为对象已经指向内存空间了,B 认为对象已存在,直接执行 return dclLazyMan 了,但是此时 DCLLazyMan 还没有完成构造,这就有问题了。

改进方法,添加 volatile 关键字,避免指令重排。

package single;

public class DCLLazyMan {

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                    /**
                     * 不是原子性操作;
                     * 1.分配内存空间;
                     * 2.执行构造方法,初始化对象;
                     * 3.把对象指向这个内存空间。
                     *
                     *  涉及指令重排,
                     *  我们期望顺序是 123 ,但底层可能执行的是 132 ;
                     *  单线程是可以的。
                     *  多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
                     * */
                }
            }
        }
        return dclLazyMan;
    }
}

这样就是一个比较好的饿汉式了。

4.反射破坏(先正常获取实例,再反射创建实例)

添加了双重检测锁 + 原子性操作,基本就OK了,但是呢,道高一尺魔高一丈,反射可以破坏这种方式。

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class DCLLazyMan {

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //多线程
        /*for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }*/

        DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();// 正常获取
        //反射获取
        Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);//无视私有构造器
        DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象


        System.out.println("dclLazyMan1 = " + dclLazyMan1);
        System.out.println("dclLazyMan2 = " + dclLazyMan2);

        // dclLazyMan1 和  dclLazyMan2 不一样,反射破坏了这种单例
    }
}

运行结果,dclLazyMan1 和 dclLazyMan2 不一样,说明这种单例被破坏了。

dclLazyMan1 = single.DCLLazyMan@4554617c
dclLazyMan2 = single.DCLLazyMan@74a14482

破坏的顺序是,先正常获取实例,再反射创建实例。
针对这种破坏,改善方法是在构造函数中判断示例是否已创建,如果已创建,说明是有人想搞破坏,抛出异常。

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class DCLLazyMan {

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        synchronized (DCLLazyMan.class){
            if (dclLazyMan != null){
                throw new RuntimeException("禁止使用反射创建");
            }
        }

        //System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //多线程
        /*for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }*/

        DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
        //反射
        Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);//无视私有构造器
        DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象


        System.out.println("dclLazyMan1 = " + dclLazyMan1);
        System.out.println("dclLazyMan2 = " + dclLazyMan2);

        // dclLazyMan1 和  dclLazyMan2 不一样,反射破坏了这种单例
    }
}

运行结果,可以看到是有效的。

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:408)
	at single.DCLLazyMan.main(DCLLazyMan.java:59)
Caused by: java.lang.RuntimeException: 禁止使用反射创建
	at single.DCLLazyMan.<init>(DCLLazyMan.java:14)
	... 5 more

5.反射破坏(直接反射创建实例)

4中反射的破坏的顺序是,先正常获取实例,再反射创建实例。
试试,不正常获取,直接用反射创建两个实例,

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class DCLLazyMan {

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        synchronized (DCLLazyMan.class){
            if (dclLazyMan != null){
                throw new RuntimeException("禁止使用反射创建");
            }
        }

        //System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //多线程
        /*for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }*/

        //DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
        //反射
        Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);//无视私有构造器
        DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
        DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象


        //System.out.println("dclLazyMan1 = " + dclLazyMan1);
        System.out.println("dclLazyMan2 = " + dclLazyMan2);
        System.out.println("dclLazyMan3 = " + dclLazyMan3);
    }
}

运行结果如下,好家伙,又被破坏了。

dclLazyMan2 = single.DCLLazyMan@4554617c
dclLazyMan3 = single.DCLLazyMan@74a14482

针对这种破坏,再改进下,在构造函数里添加标志位判断,

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class DCLLazyMan {

    public static boolean enable = false;

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        synchronized (DCLLazyMan.class){
            if (enable == false) {//首次创建
                enable = true;
            } else{
                throw new RuntimeException("禁止使用反射创建");
            }
        }

        //System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                    /**
                     * 不是原子性操作;
                     * 1.分配内存空间;
                     * 2.执行构造方法,初始化对象;
                     * 3.把对象指向这个内存空间。
                     *
                     *  涉及指令重排,
                     *  我们期望顺序是 123 ,但底层可能执行的是 132 ;
                     *  单线程是可以的。
                     *  多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
                     * */
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //多线程
        /*for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }*/

        //DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
        //反射
        Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);//无视私有构造器
        DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象
        DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象


        //System.out.println("dclLazyMan1 = " + dclLazyMan1);
        System.out.println("dclLazyMan2 = " + dclLazyMan2);
        System.out.println("dclLazyMan3 = " + dclLazyMan3);

        // dclLazyMan1 和  dclLazyMan2 不一样,反射破坏了这种单例
    }
}

运行结果如下,可以看到,阻止了这种反射情况。

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:408)
	at single.DCLLazyMan.main(DCLLazyMan.java:64)
Caused by: java.lang.RuntimeException: 禁止使用反射创建
	at single.DCLLazyMan.<init>(DCLLazyMan.java:18)
	... 5 more

但是呢,如果 enable 也被反射修改,关注这一段 myField.set(dclLazyMan2, false)

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class DCLLazyMan {

    public static boolean enable = false;

    // 使用 volatile 避免指令重排
    private volatile static DCLLazyMan dclLazyMan;

    private DCLLazyMan(){
        synchronized (DCLLazyMan.class){
            if (enable == false) {//首次创建
                enable = true;
            } else{
                throw new RuntimeException("禁止使用反射创建");
            }
        }

        //System.out.println(Thread.currentThread().getName() + ", " + toString());
    }

    // 双重检测锁加原子性操作
    public static DCLLazyMan getInstance(){
        if (dclLazyMan == null) {
            synchronized (DCLLazyMan.class){
                if (dclLazyMan == null) {
                    dclLazyMan = new DCLLazyMan();
                    /**
                     * 不是原子性操作;
                     * 1.分配内存空间;
                     * 2.执行构造方法,初始化对象;
                     * 3.把对象指向这个内存空间。
                     *
                     *  涉及指令重排,
                     *  我们期望顺序是 123 ,但底层可能执行的是 132 ;
                     *  单线程是可以的。
                     *  多线程的话, A 线程执行到 13 ,B 线程进来,因为对象已经指向内存空间了,B 认为对象已存在,直接 return 了,但是此时对象还没有完成构造
                     * */
                }
            }
        }
        return dclLazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        //多线程
        /*for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLLazyMan.getInstance();
                }
            }).start();
        }*/

        //DCLLazyMan dclLazyMan1 = DCLLazyMan.getInstance();
        //反射
        Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);//无视私有构造器

        Field myField = DCLLazyMan.class.getDeclaredField("enable");
        myField.setAccessible(true);

        DCLLazyMan dclLazyMan2 = constructor.newInstance();//创建对象

        myField.set(dclLazyMan2, false);

        DCLLazyMan dclLazyMan3 = constructor.newInstance();//创建对象


        //System.out.println("dclLazyMan1 = " + dclLazyMan1);
        System.out.println("dclLazyMan2 = " + dclLazyMan2);
        System.out.println("dclLazyMan3 = " + dclLazyMan3);

        // dclLazyMan1 和  dclLazyMan2 不一样,反射破坏了这种单例
    }
}

运行结果,

dclLazyMan2 = single.DCLLazyMan@74a14482
dclLazyMan3 = single.DCLLazyMan@1540e19d

可以看到,这种保护手段就没用了,吐血、当场 go die 。

看下 Constructor.newInstance 方法,

@CallerSensitive
    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"); // 注释 5
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

注释 5 :如果时枚举,抛出异常。so ,为了防止反射,用枚举吧。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值