使用枚举来写出更优雅的单例设计模式

作者 : 开心源码 本文共2281个字,预计阅读时间需要6分钟 发布时间: 2022-05-12 共208人阅读

Java 中的单例设计模式,很多时候我们只会注意到线程引起的表象性问题,但是没考虑过对反射机制的限制,此文旨在简单详情利用枚举来防止反射的漏洞。

一、最常见的单例

我们先展现一段最常见的懒汉式的单例:

public class Singleton {    private Singleton(){} // 私有构造    private static Singleton instance = null; // 私有单例对象    // 静态工厂    public static Singleton getInstance(){        if (instance == null) { // 双重检测机制            synchronized (Singleton.class) { // 同步锁                if (instance == null) { // 双重检测机制                    instance = new Singleton();                }            }        }        return instance;    }}

上述单例的写法采用的双重检查机制添加了肯定的安全性,但是没有考虑到 JVM 编译器的指令重排

二、杜绝 JVM 的指令重排对单例造成的影响

1、什么是指令重排

比方 java 中简单的一句 instance = new Singleton,会被编译器编译成如下 JVM 指令:

memory =allocate();    //1:分配对象的内存空间 ctorInstance(memory);  //2:初始化对象 instance =memory;     //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序:

memory =allocate();    //1:分配对象的内存空间 instance =memory;     //3:设置instance指向刚分配的内存地址 ctorInstance(memory);  //2:初始化对象

2、影响

对应到上文的单例模式,会产生如下图的问题:

  1. 当线程 A 执行完1,3,时,准备走2,即 instance 对象还未完成初始化,但已经不再指向 null 。

  2. 此时假如线程 B 抢占到CPU资源,执行 if(instance == null)的结果会是 false,

  3. 从而返回一个没有初始化完成的instance对象

3、处理

如何去防止呢,很简单,可以利用关键字 volatile 来修饰 instance 对象,如下图进行优化:

why?

很简单,volatile 修饰符在此处的作用就是阻止变量访问前后的指令重排,从而保证了指令的执行顺序。

意思就是,指令的执行顺序是严格按照上文的 1、2、3 来执行的,从而对象不会出现中间态。

其实,volatile 关键字在多线程的开发中应用很广,暂不赘述。

尽管很赞,但是此处依然没有考虑过反射机制带来的影响

三、进阶篇,实现完美单例

1、小插曲

实现单例有很多种模式,在此详情一种使用静态内部类实现单例模式的方式:

public class Singleton {    private static class LazyHolder {        private static final Singleton INSTANCE = new Singleton();    }    private Singleton (){}    public static Singleton getInstance() {        return LazyHolder.INSTANCE;    }}

这是一种很巧妙的方式,原由是:

  1. 从外部无法访问静态内部类 LazyHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。

  2. INSTANCE 对象初始化的时机并不是在单例类 Singleton 被加载的时候,而是在调用 getInstance 方法,使得静态内部类 LazyHolder 被加载的时候。

  3. 因而这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

2、漏洞展现

很多种单例的写法都有一个通病,就是无法防止反射机制的漏洞,从而无法保证对象的唯一性,如下举例:

利用如下的反正代码对上文构造的单例进行对象的创立。

public static void main(String[] args) {    try {        //取得构造器        Constructor con = Singleton.class.getDeclaredConstructor();        //设置为可访问        con.setAccessible(true);        //构造两个不同的对象        Singleton singleton1 = (Singleton)con.newInstance();        Singleton singleton2 = (Singleton)con.newInstance();        //验证能否是不同对象        System.out.println(singleton1);        System.out.println(singleton2);        System.out.println(singleton1.equals(singleton2));    } catch (Exception e) {        e.printStackTrace();    }}

我们直接看结果:

结果很显著,这显然是两个对象。

3、处理

使用枚举来实现单例模式。

实现很简单,就三行代码:

public enum Singleton {    INSTANCE;}

上面所展现的就是一个单例,

why?

其实这就是 enum 的一块语法糖,JVM 会阻止反射获取枚举类的私有构造方法

依然使用上文的反射代码来进行测试,发现,报错。嘿嘿,完美处理反射的问题。

4、缺点

使用枚举的方法是起到了单例的作用,但是也有一个弊端,

那就是 无法进行懒加载

原文地址:http://www.jetchen.cn/java-singleton-enum/

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 使用枚举来写出更优雅的单例设计模式

发表回复