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

Java 中的单例设计模式,不少时候咱们只会注意到线程引发的表象性问题,可是没考虑过对反射机制的限制,此文旨在简单介绍利用枚举来防止反射的漏洞。java

1、最多见的单例

咱们先展现一段最多见的懒汉式的单例:设计模式

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 编译器的指令重排安全

2、杜绝 JVM 的指令重排对单例形成的影响

一、什么是指令重排

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

memory =allocate();    //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance =memory;     //3:设置instance指向刚分配的内存地址
复制代码

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

memory =allocate();    //1:分配对象的内存空间 

instance =memory;     //3:设置instance指向刚分配的内存地址 

ctorInstance(memory);  //2:初始化对象
复制代码

二、影响

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

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

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

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

三、解决

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

why?

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

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

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

虽然很赞,可是此处仍然没有考虑过反射机制带来的影响

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

一、小插曲

实现单例有不少种模式,在此介绍一种使用静态内部类实现单例模式的方式:

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的加载机制来实现懒加载,并保证构建单例的线程安全。

二、漏洞展现

不少种单例的写法都有一个通病,就是没法防止反射机制的漏洞,从而没法保证对象的惟一性,以下举例:

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

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();
    }

}
复制代码

咱们直接看结果:

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

三、解决

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

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

public enum Singleton {
    INSTANCE;
}
复制代码

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

why?

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

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

四、缺点

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

那就是  没法进行懒加载

原文地址:www.jetchen.cn/java-single…

相关文章
相关标签/搜索