23种设计模式(一)单例模式

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式一个重要的思想就是构造器私有。

注意

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
    -为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

饿汉式单例

饿汉式单例一上来就会加载对象。因此可能造成内存浪费。

public class Hungry {
	//构造器私有
    private Hungry(){

    }
	// 主动创建指向自己实例的私有静态引用
    private final static Hungry HUNGRY=new Hungry();
	// 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

懒汉式单例

public class LazyMan {
    //构造器私有
    private LazyMan() {
    	System.out.println(Thread.currentThread().getName()+"ok");
    }
    // 指向自己实例的私有静态引用
    private static LazyMan lazyMan;
    
    public static LazyMan getInstance(){
    	// 被动创建,在真正需要使用时才去创建
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
    
}

我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。

多线程情况下我们做如下测试:

public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

输出:
在这里插入图片描述
我们发现在多线程并发的时候就不能保证单例了。这是因为在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

我们的开发环境肯定都是多线程并发的,那么如何解决这个问题呢?我们可以加锁,因此提出了一种双重检测锁模式的懒汉式单例

双重检测锁模式的懒汉式单例(DCL懒汉式)

public class LazyMan {
    //构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private static LazyMan lazyMan;

    public volatile static LazyMan getInstance(){
        //先判断是否存在,不存在再加锁处理
        if(lazyMan==null){
            synchronized (LazyMan.class){
                // 被动创建,在真正需要使用时才去创建
                if(lazyMan==null){
                    lazyMan=new LazyMan();//不是原子性操作
                }
            }
        }
        return lazyMan;
    }
   }

我们进行了两次if (lazyMan == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (lazyMan == null),直接return实例化对象。

使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率

优点:线程安全;延迟加载;效率较高。

如果不加volatile可以吗?答案是否定的。因为lazyMan=new LazyMan();不是一个原子性操作,他会经过三步:

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

在这个过程中会出现指令重排序的现象;如果线程A指令重排序后按照132 的顺序执行,在执行完3之后,执行2之前,有一条线程B进来判断,会发现lazyMan不为空,那么他就会直接返回lazyMan,但是此时lazyMan还没有初始化 ,为了避免指令重排序,要加上volatile。

静态内部类实现

public class Holder {

    //构造器私有
    private Holder() {
    }
    
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }
    
    //创建静态内部类
    public static class InnerClass{
    	//在静态内部类中初始化实例
        private static final Holder HOLDER=new Holder();
    }
}

静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

通过反射破坏单例

这些都是不安全的,因为存在反射。我们可以通过以下代码来破坏这个单例
破坏代码:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyMan lazyMan=LazyMan.getInstance();

        //获取LazyMan类的空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //无视私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        LazyMan lazyMan1 = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }

通过输出发现:两个对象是不同的,说明单例被破坏了
在这里插入图片描述
但是这种破坏很好解决,只需要在私有的构造方法上加一句判断即可

public class LazyMan {
    //构造器私有
    private LazyMan() {

        synchronized (LazyMan.class){
            if(lazyMan!=null){
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        //先判断是否存在,不存在再加锁处理
        if(lazyMan==null){
            synchronized (LazyMan.class){
                // 被动创建,在真正需要使用时才去创建
                if(lazyMan==null){
                    lazyMan=new LazyMan();
                }
            }
        }

        return lazyMan;
    }
}

这时候我们在执行上面的破坏代码就会发现抛出了异常
在这里插入图片描述

在我们的破坏代码中我们是先调用getInstance()构造了一个实例,然后通过反射有构造了一个实例。那么如果我们没有调用默认的构造方法创建对象,两个对象都是通过反射创建又是什么样的呢?

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //获取LazyMan类的空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //无视私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        LazyMan lazyMan=declaredConstructor.newInstance();
        LazyMan lazyMan1 = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }

输出结果:
在这里插入图片描述
我们发现单例模式又被破坏了。
这种破坏我们可以通过定义一个变量来解决,他可以是我们自己定义的一段密钥,也可以是布尔类型,总之,这个变量只有我们自己知道,不能对外泄露。以下代码定义为flag

public class LazyMan {

    private static boolean flag=false;

    //构造器私有
    private LazyMan() {

        synchronized (LazyMan.class){
            if(flag==false){
                flag=true;
            } else {
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        //先判断是否存在,不存在再加锁处理
        if(lazyMan==null){
            synchronized (LazyMan.class){
                // 被动创建,在真正需要使用时才去创建
                if(lazyMan==null){
                    lazyMan=new LazyMan();
                }
            }
        }

        return lazyMan;
    }
}

再次尝试通过破坏代码破坏单例输出:
在这里插入图片描述

我们这时候发现抛出了异常表示我们反破坏成功了。
我们所定义的变量(标志位)在不通过反编译的情况下是不会被找到的,如果标志位可以做一些加密的处理会更安全。

可是在厉害的加密,也有大佬可以破解,倘若我知道了你的标志位名称为flag,那么我照样可以通过反射把标志位也破坏掉。
可破坏标志位的破坏代码:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {

        //通过反射获取名为flag的变量
        Field flag = LazyMan.class.getDeclaredField("flag");
        //无视private
        flag.setAccessible(true);
        //获取变量的值
        Boolean aBoolean = Boolean.valueOf((Boolean) flag.get(Object));
        //获取LazyMan类的空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //无视私有的构造器
        declaredConstructor.setAccessible(true);
        //通过反射创建对象
        LazyMan lazyMan=declaredConstructor.newInstance();
        //恢复原来的值
        flag.set(lazyMan,aBoolean);
        LazyMan lazyMan1 = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }

在这里插入图片描述
通过结果我们发现我们的单例又双叒的被破坏了,那么这个万恶的反射该如何解决呢?
解决问题最根本的办法:看源码!
我们打开newInstance的源码:
在这里插入图片描述

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

通过这段代码就会发现当使用枚举时,反射是不能破坏的(枚举自带单例模式)

枚举式单例(JDK1.5之后)

public enum EnumSingle {
    //默认就是单例的
    INSTANCE;
    
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) {
        EnumSingle enumSingle1=EnumSingle.INSTANCE;
        EnumSingle enumSingle2=EnumSingle.INSTANCE;

        System.out.println(enumSingle1);
        System.out.println(enumSingle2);
        
    }
}

输出结果:
在这里插入图片描述
从源码得知反射不能破坏枚举,我们就想破坏一下

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle enumSingle1=EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle2 = declaredConstructor.newInstance();
        System.out.println(enumSingle1);
        System.out.println(enumSingle2);
    }
}

执行代码发现:
在这里插入图片描述
我们发现报的错误并不是“Cannot reflectively create enum objects”,而是“NoSuchMethodException: com.leetCode.designPattern.single.EnumSingle.()”。告诉我们没有一个空参的构造方法。
其实,在这里他的构造方法是有参的,分别为String和int。我们修改代码重新探索

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle enumSingle1=EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle2 = declaredConstructor.newInstance();
        System.out.println(enumSingle1);
        System.out.println(enumSingle2);
    }
}

输出结果:
在这里插入图片描述
至此,我们得到了想要的结果,的确抛出了反射不能破坏枚举的异常。让我们知道反射的确是不能破坏枚举单例的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值