Java单例的实现和分析(双重校验锁的破坏)

本文深入探讨了Java单例模式的实现方式,包括饿汉式、懒汉式和双重校验锁(DCL)。详细分析了DCL的线程安全性和使用volatile关键字的原因。此外,还讨论了如何通过反射破坏单例模式,并提出了通过信号量防止破坏的解决方案。最后,介绍了使用枚举实现单例模式的安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.基本概念

目的: 保证类在内存中只有一个对象,可以直接访问,不需要实例化该类的对象
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项: getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

2.代码实现

2.1.饿汉式(即时创建对象)

这种方式比较常用,但容易产生垃圾对象。空间时间
是否多线程安全:
实现难度:
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存。

public class Main {
    public static void main(String[] args) {
        //通过方法获得唯一对象
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //判断是否为唯一对象
        System.out.println(s1 == s2);//运行结果为true
    }
}
class Singleton {
    //1.私有构造方法,其他类不能访问该构造方法
    private Singleton() {}
    //2.创建本类对象
    private static Singleton s = new Singleton();
    //3.对外提供公共的访问方法
    public static Singleton getInstance() {//获取实例
        return s;
    }
}

2.2.懒汉式(使用时才创建对象)

2.2.1.线程不安全的懒汉式

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式,不要求线程安全,在多线程不能正常工作。
是否多线程安全:
实现难度:

public class Main {
    public static void main(String[] args) {
    	//通过方法获得唯一对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有构造方法,其他类不能访问该构造方法
    private SingletonLazy() {}
    //2.声明一个引用
    private static SingletonLazy s;
    //3.对外提供公共的访问方法
    public static SingletonLazy  getInstance() {//获取实例
        //当没有对象时,进行创建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.2.2.线程安全的懒汉式

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
是否多线程安全:
实现难度:
优点: 第一次调用才初始化,避免内存浪费。
缺点: 必须加锁 synchronized 才能保证单例,但加锁会影响效率。

public class Main {
    public static void main(String[] args) {
    	//通过方法获得唯一对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有构造方法,其他类不能访问该构造方法
    private SingletonLazy() {}
    //2.声明一个引用
    private static SingletonLazy s;
    //3.对外提供公共的访问方法(使用synchronized锁,防止线程抢占)
    public static synchronized SingletonLazy  getInstance() {//获取实例
        //当没有对象时,进行创建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.3.final实现方式(了解)

通过final关键字,实现对象在创建之后不可被更改。

public class Main {
    public static void main(String[] args) {
		//获得唯一对象
        Singletonfinal s1 = Singletonfinal.s;
        Singletonfinal s2 = Singletonfinal.s;
        System.out.println(s1 == s2);//结果为true
    }
}
class Singletonfinal{
    //1.私有构造方法,其他类不能访问该构造方法
    private Singletonfinal() {}
    //2.声明一个引用,final表示了地址不可被修改
    public static final Singletonfinal s = new Singletonfinal();
}

3.双重校验锁(DCL,即double-checked locking)

3.1.DCL代码实现

是否 Lazy 初始化:
是否多线程安全:
实现难度: 较复杂
描述: 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。

public class Main {
    public static void main(String[] args) {
        SingletonDCL s1 = SingletonDCL.getInstance();
        SingletonDCL s2 = SingletonDCL.getInstance();
        System.out.println(s1 == s2);//结果是true
    }
}
class SingletonDCL {
    //1.构造私有方法
    private SingletonDCL() {}
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

3.2.使用volatile的原因(禁止指令重排序)

指令重排序: 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
例如:

 singletonDCL = new SingletonDCL();//实例化对象这行代码

这行代码实际上可以分解成三个步骤:
1.分配内存空间。
2.初始化对象。
3.将对象指向刚分配的内存空间。

但是有些编译器因为性能的原因,可能会改变2和3的顺序,就成了:
1.分配内存空间。
2.将对象指向刚分配的内存空间。
3.初始化对象。

在不使用volatile且发生重排序的情况下,调用顺序如下

线程一线程二
检查到singletonDCL为null-
获取锁-
再次检查到singletonDCL为null-
为singletonDCL分配内存空间-
将singletonDCL指向内存空间-
-检查到singletonDCL不为空
-访问singletonDCL(此时线程一还未初始化完成对象)
初始化singletonDCL-

在这种情况下,在线程二访问singletonDCL时,访问的是一个初始化未完成的对象。

4.如何通过反射破坏双重校验锁(抬杠)

4.1.1.通过反射无视私有构造器创建对象

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingletonDCL s1 = SingletonDCL.getInstance();
        SingletonDCL s2 = SingletonDCL.getInstance();
        System.out.println(s1);
        System.out.println(s2);
       //---------------以下为添加的部分,SingletonDCL部分未改动--------------------
        //通过字节码,获得SingletonDCL的空参构造器
        Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
        //无视了私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建SingletonDCL对象
        SingletonDCL s3 = declaredConstructor.newInstance();
        //输出对象
        System.out.println(s3);
    }
}
class SingletonDCL {
    //1.构造私有方法
    private SingletonDCL() {}
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

运行结果:可见s1,s2为同一个对象,通过反射走对象空构造器创建的s3为不同的对象。
在这里插入图片描述

4.1.2.如何防止通过4.1.1反射机制创建对象

思路:如果对象已经走getInstance()进行创建,则可以在空构造器中加入对象判断,防止通过反射再次创建。

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingletonDCL s1 = SingletonDCL.getInstance();
        SingletonDCL s2 = SingletonDCL.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        //通过字节码,获得SingletonDCL的空参构造器
        Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
        //无视了私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        SingletonDCL s3 = declaredConstructor.newInstance();
        System.out.println(s3);
    }
}
class SingletonDCL {
    //1.构造私有方法
    private SingletonDCL() {
        synchronized (SingletonDCL.class){
       //---------------以下为添加的部分,增加第三重检测--------------------
            //第三重检测,如果对象singletonDCL已被创建,则可认为有人想通过反射进行创建
            if(singletonDCL!=null){
                throw new RuntimeException("使用反射创建对象失败");
            }
        }
    }
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

运行结果是这样的,原因是在一开始getInstance()的时候已经存在了singletonDCL对象,在第三重检测的时候,发现对象已经创建,所以就会走Exception。
在这里插入图片描述

4.1.3.使用多次反射破坏双重校验锁(杠到底)

在4.1.2的方法中,第三重校验能成功,是因为在一开始走了getInstance()方法,已经创建了singletonDCL对象,所以通过反射走空构造器的时候会失败。但是如果只用反射创建对象,singletonDCL对象并没有存储,得到的依旧是非单例的结果。

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //通过字节码,获得SingletonDCL的空参构造器
        Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
        //无视了私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        SingletonDCL s3 = declaredConstructor.newInstance();
        SingletonDCL s4 = declaredConstructor.newInstance();
        System.out.println(s3);
        System.out.println(s4);
    }
}
class SingletonDCL {

    //1.构造私有方法
    private SingletonDCL() {
        synchronized (SingletonDCL.class){
            //第三重检测,如果对象singletonDCL已被创建,则可认为有人想通过反射进行创建
            if(singletonDCL!=null){
                throw new RuntimeException("使用反射创建对象失败");
            }
        }
    }
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

运行结果:通过反射创建的s3,s4
在这里插入图片描述

4.1.4.通过信号量保证单例对象只创建一次

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //通过字节码,获得SingletonDCL的空参构造器
        Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
        //无视了私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        SingletonDCL s3 = declaredConstructor.newInstance();
        System.out.println(s3);
        SingletonDCL s4 = declaredConstructor.newInstance();
        System.out.println(s4);
    }
}
class SingletonDCL {
//---------------以下为添加的部分--------------------
   //定义一个对象是否创建的判断信号
    private static boolean charge= false;

    //1.构造私有方法
    private SingletonDCL() {
        System.out.println("走空构造器");
        synchronized (SingletonDCL.class){
            //---------------以下为添加的部分--------------------
            //无论走反射或者是get方法,创建一次对象之后,此信号就会置为true
            if (charge ==false){
                charge =true;
            }else {
                throw new RuntimeException("使用反射创建对象失败");
            }
        }

    }
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

运行结果:在第二次走空构造器的时候,由于charge为true,所以走else报错。
在这里插入图片描述

5.通过枚举实现单例模式

枚举是一种自带单例模式的类型,具体实现可以点进源码了解。

public enum Singleton {
    //仅枚举INSTANCE
    INSTANCE;
}
class Main2{
    public static void main(String[] args) {
        System.out.println(Singleton.INSTANCE);
        System.out.println(Singleton.INSTANCE);
    }
}

运行结果:显而易见,只能为单例
在这里插入图片描述

6.总结

在4.1.4.的方法可以通过反射破坏私有,获得charge变量,将其值改为false再次进行对象创建。因此看来似乎并没有绝对安全的代码。单例实现过程的话可以考虑看看枚举类型->enum的源码。另一方面,反序列化也可以破坏双重校验锁的单例实现。可谓道高一尺魔高一丈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值