急了急了,破防单例模式

本文主要介绍单例创建的集中方式和反射给单例造成的影响。

单例的定义

单例模式:保证一个类仅有一个实例对象,并且提供一个全局访问点。

单例的特点

  • 单例类只能有一个实例对象
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须对外提供一个访问该实例的方法

使用场景及优点

优:

  • 提供了对唯一实例的受控访问
  • 保证了内存中只有唯一实例,减少内存开销,比如需要多次创建和销毁实例的场景
  • 避免对资源的多重占用,比如文件的写操作

缺:

  • 没有抽象层,接口,不能继承,扩展困难,违反了开闭原则
  • 单例类一般写在同一个类中,职责过重,违背了单一职责原则

应用场景:

文件系统;数据库连接池的设计;日志系统等 IO/生成唯一序列号/身份证/对象需要共享的情况,比如web中配置对象

实现单例

三步:

  1. 构造函数私有化
  2. 在类内部创建实例
  3. 提供本类实例的唯一全局访问点,即唯一实例的方法
饿汉式:
public class Hungry {
    // 构造器私有,静止外部new
    private Hungry(){}

    // 在类的内部创建自己的实例
    private static Hungry hungry = new Hungry();

    // 获取本类实例的唯一全局访问点
    public static Hungry getHungry(){
        return hungry;
    }
}

懒汉式:
public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            lazy1 = new Lazy1();
        }
        return lazy1;
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy1.getLazy1();
            }).start();
        }
    }
}

单线程环境下是没有问题的,但是多线程的情况下就会出现问题

DCL 懒汉式:

方法上直接加锁:

public static synchronized Lazy1 getLazy1(){
    if (lazy1 == null) {
        lazy1 = new Lazy1();
    }
    return lazy1;
}

缩小锁范围:

public static Lazy1 getLazy1(){
    if (lazy1 == null) {
        synchronized(Lazy1.class){
            lazy1 = new Lazy1();
        }
    }
    return lazy1;
}

双重锁定:

// 获取本类实例的唯一全局访问点
public static Lazy1 getLazy1(){
    // 如果实例不存在则new一个新的实例,否则返回现有的实例
    if (lazy1 == null) {
        // 加锁
        synchronized(Lazy1.class){
            // 第二次判断是否为null
            if (lazy1 == null){
                lazy1 = new Lazy1();
            }
        }
    }
    return lazy1;
}

指令重排序: 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。

首先要知道 lazy1 = new Lazy1(); 这一步并不是一个原子性操作,也就是说这个操作会分成很多步

① 分配对象的内存空间 ② 执行构造函数,初始化对象 ③ 指向对象到刚分配的内存空间

但是 JVM 为了效率对这个步骤进行了重排序,例如这样:

① 分配对象的内存空间 ③ 指向对象到刚分配的内存空间,对象还没被初始化 ② 执行构造函数,初始化对象

解决的方法很简单——在定义时增加 volatile 关键字,避免指令重排

最终代码:

public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy1.getLazy1();
            }).start();
        }
    }
}

静态内部类懒汉式单例:

双重锁定算是一种可行不错的方式,而静态内部类就是一种更加好的方法,不仅速度较快,还保证了线程安全,先看代码:

public class Lazy2 {
    // 构造器私有,静止外部new
    private Lazy2(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 用来获取对象
    public static Lazy2 getLazy2(){
        return InnerClass.lazy2;
    }

    // 创建内部类
    public static class InnerClass {
        // 创建单例对象
        private static Lazy2 lazy2 = new Lazy2();
    }

    public static void main(String[] args) {
        // 多线程访问,看看会有什么问题
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy2.getLazy2();
            }).start();
        }
    }
}

上面的代码,首先 InnerClass 是一个内部类,其在初始化时是不会被加载的,当用户执行了 getLazy2() 方法才会加载,同时创建单例对象,所以他也是懒汉式的方法,因为 InnerClass 是一个静态内部类,所以只会被实例化一次,从而达到线程安全,因为并没有加锁,所以性能上也会很快。

枚举创建单例:

public enum EnumSingle {
    IDEAL;
}

代码就这样,简直不要太简单,访问通过 EnumSingle.IDEAL 就可以访问了


反射破坏单例模式

单例是如何被破坏的:

这是我们原来的写法,new 两个实例出来,输出一下

public class Lazy1 {
    // 构造器私有,静止外部new
    private Lazy1(){
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

    public static void main(String[] args) {

        Lazy1 lazy1 = getLazy1();
        Lazy1 lazy2 = getLazy1();
        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}

运行结果: main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@1b6d3586

可以看到,结果是单例没有问题

一个普通实例化,一个反射实例化:
public static void main(String[] args) throws Exception {
    Lazy1 lazy1 = getLazy1();
    // 获得其空参构造器
    Constructor  declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
    // 使得可操作性该 declaredConstructor 对象
    declaredConstructor.setAccessible(true);
    // 反射实例化
    Lazy1 lazy2 = declaredConstructor.newInstance();
    System.out.println(lazy1);
    System.out.println(lazy2);
}

运行结果:

main 访问到了 main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@4554617c

可以看到,单例被破坏了

如何解决:因为我们反射走的其无参构造,所以在无参构造中再次进行非null判断,加上原来的双重锁定,现在也就有三次判断了。
解决方案:增加一个标识位,例如下文通过增加一个布尔类型的 ideal 标识,保证只会执行一次,更安全的做法,可以进行加密处理,保证其安全性。

这样就没问题了吗,并不是,一旦别人通过一些手段得到了这个标识内容,那么他就可以通过修改这个标识继续破坏单例,代码如下(这个把代码贴全一点,前面都是节选关键的,都可以参考这个)

public class Lazy1 {

    private static boolean ideal = false;

    // 构造器私有,静止外部new
    private Lazy1(){
        synchronized (Lazy1.class){
            if (ideal == false){
                ideal = true;
            } else {
                throw new RuntimeException("反射破坏单例异常");
            }
        }
        System.out.println(Thread.currentThread().getName() + " 访问到了");
    }

    // 定义即可,不真正创建
    private static volatile Lazy1 lazy1 = null;

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }

    public static void main(String[] args) throws Exception {

        Field ideal = Lazy1.class.getDeclaredField("ideal");
        ideal.setAccessible(true);

        // 获得其空参构造器
        Constructor declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
        // 使得可操作性该 declaredConstructor 对象
        declaredConstructor.setAccessible(true);
        // 反射实例化
        Lazy1 lazy1 = declaredConstructor.newInstance();
        ideal.set(lazy1,false);
        Lazy1 lazy2 = declaredConstructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}

运行结果: main 访问到了 main 访问到了 cn.ideal.single.Lazy1@4554617c cn.ideal.single.Lazy1@74a14482 实例化 lazy1 后,其执行了修改 ideal 这个布尔值为 false,从而绕过了判断,再次破坏了单例 所以,可以得出,这几种方式都是不安全的,都有着被反射破坏的风险。

你可能感兴趣的:(急了急了,破防单例模式)