怀旧网,博客详情:设计模式--单例模式

1、设计模式介绍

2、设计模式--单例模式

3、设计模式--工厂模式

4、设计模式--抽象工厂模式

5、设计模式--建造者模式

6、设计模式--原型模式

7、设计模式--适配器模式

8、设计模式--桥接模式

9、设计模式--代理模式

原创

设计模式--单例模式

单例模式介绍

核心作用:

  • 保证一个类只有一个实例,并且提供一个访问该实例的全局访问点

常见场景:

image-20240604095835743

1)饿汉式

// 饿汉式单例类
class Hungry {
    // 一上来就实例化,可能会浪费空间
    private final static Hungry HUNGRY = new Hungry();

    // 私有化构造器
    private Hungry(){}

    // 定义一个方法返回单例对象
    public Hungry getInstance() {
        return HUNGRY;
    }
}

2)DCL懒汉式

// 懒汉式单例
class LazyMan {
    // 在第一次使用才复制(节省空间)
    private static LazyMan lazyMan;

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

    // 调用方法获取单例对象
    public static LazyMan getInstance() {
        // 当第一次访问为 null 的时候才初始化
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        
        return lazyMan;
    }
}

使用测试:

public static void main(String[] args) {
    LazyMan lazyMan1 = LazyMan.getInstance();
    LazyMan lazyMan2 = LazyMan.getInstance();

    System.out.println(lazyMan1 == lazyMan2);
}

image-20240603212022759

输出结果为true:说明单例创建完成!

测试多线程情况下;

修改构造方法

//私有化构造器
private LazyMan() {
    System.out.println("LazyMan 构造方法调用");
}

编写测试代码:

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

查看输出:

image-20240603212237444

发现在多线程情况下构造方法被调用了多次,此时就破坏了单例模式!

解决方案第一步:添加创建时的锁-让getInstance()方法同时只能被一个线程调用

// 调用方法获取单例对象
public synchronized static LazyMan getInstance() {
    // 当第一次访问为 null 的时候才初始化
    if (lazyMan == null) {
        lazyMan = new LazyMan();
    }

    return lazyMan;
}

查看输出:

image-20240603212446829

成功解决了问题:

但是这种锁每次进入方法都会进行检测,很浪费效率,我们将代码修改为:双重检锁模式

// 调用方法获取单例对象
public static LazyMan getInstance() {
    // 访问的时候为 null 才进入下面代码(当不是第一次访问的时候,提高运行效率)
    if (lazyMan == null) {
        synchronized (LazyMan.class) {
            // 当第一次访问为 null 的时候才初始化
            if (lazyMan == null) {
                lazyMan = new LazyMan();
            }
        }
    }
    return lazyMan;
}

分析代码:

image-20240603212927792

它不是一个原子性操作:

1、分配内存空间 2、执行构造方法,初始化对象 3、把这个对象指向这个空间 执行顺序123,132都有可能 A:123 B:132 B把这个对象指向这个空间,发现不为空执行return 但是此时在线程A中,lazyMan还没有完成构造,lazyMan要加volatile,防止指令重排

private volatile static LazyMan lazyMan;

静态内部类方式创建单例:

class Holder {
    private Holder() {}

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

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

以上都不安全,可以通过反射破坏!

测试:

public static void main(String[] args) throws Exception{
    LazyMan instance1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan instance2 = declaredConstructor.newInstance();

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

image-20240603213920950

通过测试发现通过反射的方式,还是会破坏单例模式:

解决方案:(对构造方法在进行判断并加锁)

//私有化构造器
private LazyMan() {
    synchronized(Holder.class){
        if (lazyMan != null){
            throw new RuntimeException("当前单例对象已经创建,请不要再次尝试调用构造方法!");
        }
    }
}

运行测试:

image-20240603214404122

但是仍然可以通过如下方式破坏:

public static void main(String[] args) throws Exception{
    // LazyMan instance1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan instance1 = declaredConstructor.newInstance();
    LazyMan instance2 = declaredConstructor.newInstance();

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

image-20240603214453621

还是有bug

再次寻找解决方案:(使用一个标志位来进行加密)

private static boolean biaozhiwei = false;

//私有化构造器
private LazyMan() {
    synchronized(Holder.class){
        if(biaozhiwei == false){
            biaozhiwei = true;
        }else {
            throw new RuntimeException("当前单例对象已经创建,请不要再次尝试调用构造方法!");
        }
    }
}

运行测试:

image-20240603214734729

成功解决上面的问题:

但是在这边情况下,还是有可能被破译,要是在知道标志位名称的情况下(通过反编译进行查看属性名称,在通过反射将值进行修改)还是有漏洞:

public static void main(String[] args) throws Exception{
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan instance1 = declaredConstructor.newInstance();

    // 将标志位的值在该会为 false
    Field biaozhiwei = LazyMan.class.getDeclaredField("biaozhiwei");
    biaozhiwei.setAccessible(true);
    biaozhiwei.set(instance1, false);

    LazyMan instance2 = declaredConstructor.newInstance();

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

image-20240603215232854

再次成功破坏单例!

查看jdk自带的枚举怎么实现单例的。

3)枚举

使用枚举,我们就可以防止反射破坏了。

// enum 是一个什么? 本身也是一个Class类
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(null);
        declaredConstructor.setAccessible(true);

        //java.lang.NoSuchMethodException: com.zzy.single.EnumSingle.<init>() 没有空参构造方法
        EnumSingle instance2 = declaredConstructor.newInstance();

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

反编译

image-20240603195149872

image-20240603195155526

使用jad工具反编译为java

枚举类型的最终反编译源码:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
public final class EnumSingle extends Enum{
    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }
    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/kuang/single/EnumSingle, name);
    }
    private EnumSingle(String s, int i)
    {
        super(s, i);
    }
    public EnumSingle getInstance()
    {
        return INSTANCE;
    }
    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];
    static
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
                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);

        //java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        EnumSingle instance2 = declaredConstructor.newInstance();

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

抛出异常: java.lang.IllegalArgumentException: Cannot reflectively create enum objects

  • 平台作者:怀旧(联系作者)
  • QQ:444915368
  • 邮箱:444915368@qq.com
  • 电话:17623747368
  • 评论

    登录后才可以进行评论哦!

    回到顶部 留言