m1.002.设计模式之单例模式Singleton-饿汉式

1.单例模式的概述

单例模式是面试中问的较多也是最容易在代码中体现的一种设计模式,它是创建型设计模式中的一种。

主要的目的就是保证一个类在全局只有一个实例,并提供一个访问到该实例的全局节点。

单例模式

在刚刚对于单例模式的作用中可以看出,单例模式解决了两个问题,所以说违反了单一职责原则。

1.1 单例模式可以解决的问题(1)

保证一个类只有一个实例,控制某些共享资源(数据库/文件)的访问权限,或者说某一个对象中的属性内容特别多,每次构建的时候都要进行赋值,而且都是相同内容赋值的时候,可以将此对象变为单例的,全局共同访问一个相同的对象使用即可。

1.2 单例模式可以解决的问题(2)

为该实例提供一个全局访问的方式,全局可以在任何地方访问到该实例对象。

2.单例模式(饿汉式)设计思路
2.1 单例模式(饿汉式)的基本设计(1)-解决可以重复创建的问题

如果有一个类叫做Singleton,想要保证全局只有一个该类对象,应该怎么做?

最应该做的就是让别人不创建该类对象,因为一旦有任何一个人创建了该对象,那么全局对象就不唯一了,张三new了一个Singleton对象,李四new了一个Singleton对象就乱套了,但是我们不能禁止别人new对象,解决方式可以将构造函数定义为私有,这个样子就可以防止别人通过new创建对象。

public class Singleton {

    //保证该类只有一个无参构造方法(防止外界直接访问)
    private Singleton() {
    }

}

但是这么写本质上防止不了使用者通过构造方法访问此类并获取构造方法取消检查权限的问题,之后可以通过别的方式解决。

2.2 单例模式(饿汉式)的基本设计(2)-提供全局可以访问的公共方式
public class Singleton {

    //保证该类只有一个无参构造方法(防止外界直接访问)
    private Singleton() {
    }

    //提供一个全局可以访问的静态方法用于获取到本类对象
    public static Singleton getInstance() {
        return new Singleton(); //本类中可以访问私有构造方法
    }
}

可以发现写到这里,虽然提供了公共的访问方式,但是每次访问此静态方法都会创建一个新的Singleton对象返回,所以不符合单例的特性,解决方案就是在本类中封装了一个私有的静态的本类成员变量,然后直接进行初始化,在方法中直接返回本类成员变量即可。

public class Singleton {

    //保证该类只有一个无参构造方法(防止外界直接访问)
    private Singleton() {
    }

    //封装一个静态私有本类成员变量并初始化.
    private static Singleton singleton = new Singleton(); //本类中可以访问私有构造方法

    //提供一个全局可以访问的静态方法用于获取到本类对象
    public static Singleton getInstance() {
        return singleton;
    }
}

代码编写到这里,饿汉式的单例模式就已经编写完成了,现在在全局任意类都可以通过Singleton.getInstance();获取到本类的唯一对象了。

对于饿汉的理解就是,这个类很"饥饿",在类被加载的时候就创建好了唯一的单例对象,不是在真正需要用这个类的时候才创建。

如果单例对象的构造比较的复杂,需要进行很多的数据封装,但是使用的频率不高,其实没有必要在类加载的时候就进行创建。那么也就延伸出了另外一种的单例模式的分类也就是懒汉式,但是饿汉式在多线程下是否可以获取同一个对象?是否还会有其他问题呢?

3.单例模式(饿汉式)的多线程安全问题
3.1 单例模式(饿汉式)的多线程攻击

单例模式(饿汉式)在单线程环境下获取始终可以获取到同一个对象,多线程环境下可以通过如下代码进行测试。

public class SingletonTest {
    public static void main(String[] args) {
        //声明线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);

        //基于线程池循环提交线程任务(用于获取Singleton类对象)
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                Singleton singleton = Singleton.getInstance();
                StaticLog.info("{}线程获取到的Singleton对象是{}", Thread.currentThread().getName(), singleton);
            });
        }

        //释放线程池
        pool.shutdown();
    }
}

通过以上代码执行,可以获取到的结果是:

在这里插入图片描述

并没有出现多个线程获取到的对象不唯一的问题,因为在Singleton类加载的时候类加载的初始化阶段就已经创建好了Singleton对象,真正执行多线程代码获取的时候,已经存在了一个有具体指向Singleton$v3对象,所有线程获取到的都是已经创建好的同一个对象。这是类加载过程中加载静态内容的特点。 ​

4.单例模式(饿汉式)的反射安全问题
4.1 单例模式(饿汉式)的反射攻击

之前在编写代码的时候已经将类的构造方法私有化了,但是并不能防止反射,因为反射可以获取到任意一个类的任意内容并且执行,在反射环境下可以编写如下代码来执行单例类的私有构造创建多个对象。

public class SingletonV1ReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //获取Sinleton类的单例构造方法的Constructor对象
        Constructor<Singleton> noArgConstructor = Singleton.class.getDeclaredConstructor();

        //关闭权限校验后基于newInstance方法进行对象创建
        noArgConstructor.setAccessible(true);
        Singleton singleton1 = noArgConstructor.newInstance();
        Singleton singleton2 = noArgConstructor.newInstance();

        //查看结果
        StaticLog.info("singleton1对象的信息是:{}", singleton1);
        StaticLog.info("singleton2对象的信息是:{}", singleton2);
    }
}

在这里插入图片描述

4.2 单例模式(饿汉式)的反射攻击解决方案

对于反射破坏单例模式,其实核心就是在于如何让反射不能成功调用私有无参构造,这个由于反射特性无法阻止,但是我们在私有无参构造中进行判断,如果当前的Instance为NULL,则正常调用,如果为不为NULL,则直接抛出异常,也算某种程度上解决了反射破坏单例。

public class Singleton {

    //保证该类只有一个无参构造方法(防止外界直接访问)
    private Singleton() {
        //如果无参构造执行的发现本类的singleton成员变量不是NULL,则直接抛出运行期异常
        if (Objects.nonNull(singleton))
            throw new RuntimeException(this.getClass().getName() + "已在全局存在唯一示例,无法实例化!");
    }

    //封装一个静态私有本类成员变量并初始化.
    private static Singleton singleton = new Singleton(); //本类中可以访问私有构造方法

    //提供一个全局可以访问的静态方法用于获取到本类对象
    public static Singleton getInstance() {
        return singleton;
    }
}

如果再运行反射代码反射该类的构造方法,则会直接抛出运行期异常。

在这里插入图片描述

但是在这里要说一句,毕竟反射是人为的编写代码,本质上是可防的!

5.单例模式(饿汉式)的序列化安全问题
5.1 单例模式(饿汉式)的序列化攻击

Java的序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以被保存在磁盘上,或者通过网络传输。

对于单例模式我们可以将该类获取到的全局对象转换为字节序列保存到文件中,序列化特点:只保存对象的信息,但是不保存对象的地址。当我们再将对象反序列化读取到内存中的时候会重新构建一个该对象,但是不是使用原有地址,所以可能会导致内存中会出现两个指向不同地址的单例类对象,破坏了单例模式。

序列化攻击的前提是单例类实现了Serializable接口(否则会出现运行期异常),通过代码序列化对象到文件中再反序列化出来。

public class SingletonV1SerializableTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //获取Singleton类唯一对象后序列化后保存当前resources目录中obj文件中
        Singleton singleton = Singleton.getInstance();
        ObjectOutputStream oos
                = new ObjectOutputStream(new FileOutputStream("C:\\Users\\l1ub0\\IdeaProjects\\java-design-pattern\\src\\main\\resources\\obj"));
        oos.writeObject(singleton);
        oos.close();

        //创建反序列化流再将resource下的obj中的字节序列读取到内存中生成一个对象
        ObjectInput ois = new ObjectInputStream(new FileInputStream("C:\\Users\\l1ub0\\IdeaProjects\\java-design-pattern\\src\\main\\resources\\obj"));
        Singleton newSingleton = (Singleton) ois.readObject();
        ois.close();

        //对比序列化前和序列化后Sinleton对象的内容
        StaticLog.info("序列化前Singleton对象是:{}", singleton);
        StaticLog.info("序列化后Singleton对象是:{}", newSingleton);
    }
}

得到的结果如下:

在这里插入图片描述

我们能看到的就是序列化进去的对象和反序列化出来的对象的地址值不一样,无论内容是否相同,已经破坏了单例模式,在全局中出现了两个对象。

5.2 单例模式(饿汉式)的序列化攻击解决方案

通过对ObjectInputStream源码的观察和运行过程我们可以得到以下的过程:

在这里插入图片描述

也就是最核心的内容,在反序列化的过程中会判断要反序列化出来的对象所在的类总是否存在一个readResolve方法,如果有的话,则反序列化的过程会调用此方法将此方法的返回值作为readObject方法的返回值。

解决方案:在单例类中创建一个readResolve方法,并将全局唯一对象返回。

public class Singleton implements Serializable {

    //保证该类只有一个无参构造方法(防止外界直接访问)
    private Singleton() {
        //如果无参构造执行的发现本类的singleton成员变量不是NULL,则直接抛出运行期异常
        if (Objects.nonNull(singleton))
            throw new RuntimeException(this.getClass().getName() + "已在全局存在唯一示例,无法实例化!");
    }

    //封装一个静态私有本类成员变量并初始化.
    private static Singleton singleton = new Singleton(); //本类中可以访问私有构造方法

    //提供一个全局可以访问的静态方法用于获取到本类对象
    public static Singleton getInstance() {
        return singleton;
    }

    //提供一个readResolve(当反序列化对象时)使用此方法的逻辑进行反序列化
    public Object readResolve() {
        return singleton;
    }
}

再次尝试运行测试类,即可获取到同一个对象。

在这里插入图片描述

6.单例模式(饿汉式)Unsafe的安全问题

Unsafe类是Java中提供的类似于C++可以手动操作内存的类,从名字上来看这个类的使用是极其不安全的。但是这个类也提供了对于一个类不通过构造函数直接进行对象创建的功能,也可以用于破坏单例模式。

6.1 Unsafe类的获取方式-反射Unsafe类(构造方法)

通过反射Unsafe类的私有构造进行Unsafe对象的创建。

public static void main(String[] args) throws Exception {
    //获取Unsafe类的Class对象后获取到私有构造方法取消检查权限后创建对象
    Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
    Constructor<?> unsafePrivateConstructor = unsafeClass.getDeclaredConstructor();
    unsafePrivateConstructor.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafePrivateConstructor.newInstance();
}
6.2 Unsafe类的获取方式-反射Unsafe类(成员变量)

Unsafe类中有一个成员变量叫做theUnsafe,维护的就是一个Unsafe的对象,通过反射也可以进行获取。

public static void main(String[] args) throws Exception {
    //获取Unsafe类的Class对象后获取到私有成员变量(theUnsafe)取消检查权限后获取成员变量值
    Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
    Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
    theUnsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
}
6.3 Unsafe类的获取方式-Spring工具类

Spring框架中提供了一个UnsafeUtils的工具类,通过静态方法getUnsafe也可以获取到一个Unsafe对象(需要导入spring-core核心包)。

public class SingletonV1UnsafeTest {
    public static void main(String[] args) {
        //基于spring-core核心包中提供的工具类获取一个Unsafe对象
        Unsafe unsafe = UnsafeUtils.getUnsafe();
    }
}
6.4 单例模式(饿汉式)的Unsafe类的内存攻击

Unsafe类中提供了一个allocateInstance方法,该方法可以传递要创建对象的类的class对象进行对象创建,而且不会通过本类的构造方法,直接在内存中进行创建。

在这里插入图片描述

public class SingletonV1UnsafeTest {
    public static void main(String[] args) throws InstantiationException {
        //基于spring-core核心包中提供的工具类获取一个Unsafe对象
        Unsafe unsafe = UnsafeUtils.getUnsafe();

        //基于Unsafe类的allocateInstance方法创建Singleton类对象
        Singleton newSingleton = (Singleton) unsafe.allocateInstance(Singleton.class);

        //基于Singleton类获取全局唯一实例Singleton对象与基于Unsafe类创建的Singleton对象进行对比
        Singleton singleton = Singleton.getInstance();

        StaticLog.info("基于Singleton的getInstance方法获取到的对象是:{}", singleton);
        StaticLog.info("基于Unsafe的allocateInstance方法创建的对象是:{}", newSingleton);
    }
}

在这里插入图片描述

6.3 单例模式(饿汉式)的Unsafe类的内存攻击解决方案

Unsafe类破坏单例无法通过代码层面预防,因为这本质上就是直接操作内存而且不通过构造方法的一种方式,但代码是人写的,总体是可控的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值