单例模式介绍

2年前 (2022) 程序员胖胖胖虎阿
272 0 0

目录

1 前言

2 单例模式类型

2.1 饿汉式:

2.2 懒汉式:

2.2.1 双重检查锁

2.2.2 volatile防止指令重排

2.3 静态内部类

3 破坏单例


1 前言

单例模式是指在内存中有且只会创建一次对象的设计模式,在程序中多次使用同一个对象且作用相同的时候,为了防止频繁的创建对象,单例模式可以让程序在内存中创建一个对象,让所有的调用者都共享这一单例对象。单例模式的类型有两种:懒汉式饿汉式

2 单例模式类型

  • 饿汉式:在类加载的时候已经创建好该单例对象。
  • 懒汉式:在需要使用对象的时候才会去创建对象

2.1 饿汉式:

//饿汉式
public class Hungry {

    /**
     * 构造器私有 拒绝别人创建这个对象
     */
    private Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

大家都知道饿汉式单例是程序启动的时候就已经创建好了对象,那么这样的会会有什么问题呢?有可能浪费空间

为什么呢?因为如果在该类中声明了许多内存空间,但却没有使用的话,就很浪费内存空间,因为饿汉式它是在程序启动的时候就已经创建好了。如下:

//饿汉式
public class Hungry {

    //可能会浪费空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];
    private byte[] data5 = new byte[1024 * 1024];
    
    /**
     * 构造器私有 拒绝别人创建这个对象
     */
    private Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

那么既然饿汉式单例有问题,那么就出现了懒汉式单例模式,需要对象的时候才会去创建。

2.2 懒汉式:

//懒汉式单例
public class LazySingle {

    private LazySingle(){

    }
    private static LazySingle lazySingle;

    public static LazySingle getInstance(){
        if (lazySingle == null ) {
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

2.2.1 双重检查锁

但是这样会不会有问题呢?这样创建的懒汉式单例模式在单线程环境下肯定是没问题的,但是在多线程环境下,就会有问题了。就会不止创建一个对象了,那么如何改进呢?如下:

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }

这就是双重检查加锁的机制了(DCL懒汉式),这样就可以保证在多线程环境下有且仅会创建一个对象。

2.2.2 volatile防止指令重排

那么这样的双重检查锁是完整的吗,会不会出现一些其他的问题呢?其实也是会的,因为当我们在 new LazySingle()的时候,其实是有可能发生指令重排的。

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

正常情况下,执行的这个顺序是1,2,3,如果在发生了指令重排,并且在多线程的环境下,也会出现问题。比如:A线程指令重排1,3,2,那么在重排过程中线程B进来,发现lazySingle已经分配内存空间了,不等于null了,那么就直接返回了,对于这种情况应该怎样处理呢?可以使用volatile关键字来解决,如下:

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


}

2.3 静态内部类

除此之外,我们还可以使用静态内部类来实现:

//静态内部类
public class Holder {

    private Holder(){

    }
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

3 破坏单例

但是无论是懒汉式单例还是饿汉式单例,都可以利用反射去破坏

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        System.out.println(instance.equals(lazySingle)); //false
    }

以上就是利用反射强制访问类的私有构造器,去创建另外一个对象

那么如何去解决它呢?

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        synchronized (LazySingle.class){
            if (lazySingle != null ) {
                throw new RuntimeException("不要利用反射去破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        System.out.println(instance.equals(lazySingle));
    }
}

控制台输出:

单例模式介绍

 以上是通过正常调用了对象和通过反射调用了一次对象发现单例被破坏了,那么如果两次直接在反射中去创建对象,这样会被发现吗?

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }
}

这样可见,直接这样去操作的话,单例被破坏依然是发现不了的,那么如何去解决呢?我们可以在私有构造器中加上一个标识,根据标识去判断,如下:

//懒汉式单例
public class LazySingle {

    private static Boolean flag = false;

    private LazySingle(){
        synchronized (LazySingle.class){
            if (!flag) {
                flag = true;
            }else {
                throw new RuntimeException("不要利用反射去破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }
}

控制台输出:

单例模式介绍

这样就万无一失了吗?就能保证它不被破坏了吗,俗话说魔高一尺,道高一丈,反射依然可以去破坏它

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Field flag = LazySingle.class.getDeclaredField("flag");
        flag.setAccessible(true);
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        flag.set(lazySingle,false);
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }

 可见啊,无论我们如何去防止它,这个反射总是可以去破解我们的单例。那究竟怎样才能解决这个问题呢?那么就得看下反射到底是如何去创建一个对象的了。

单例模式介绍

发现显示不能用反射去破坏枚举,那么真的可以这样吗,我们来试一下:

枚举类:

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }


}

 测试类:

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

测试结果:

单例模式介绍

可见真的不能用反射区破坏枚举。

注意:此时枚举类中不是无参构造,而是有参构造

private EnumSingle(String s,int i) {
    super(s,i);
}
版权声明:程序员胖胖胖虎阿 发表于 2022年10月26日 下午5:48。
转载请注明:单例模式介绍 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...