• 单例模式(Java实践)


    在计算机软件中,单例的定义是在整个程序生命周期里,一个单例类的实例只能存在一个

    Java 应用里使用单例的例子

    最佳实践(推荐)

    在Joshua Bloch, Effective Java 2nd Edition p.18中给出了单例的最佳实践,使用枚举!

    // best practice
    public enum Singleton {
      INSTANCE;
    }
    

    这种方式对于从C转Java的同学来说估计很难接受,我自己刚才始也难以接受,但是Java中一切都是对象,枚举也是对象!所以枚举对象里面是可以有成员变量与方法的!枚举单例的好处是:无法通过反射与序列化创建多个实例。项目中正真使用这种方式做单例的很少,原因有两点

    • 枚举单例早期的代码里面没人使用,Effective Java 2nd Edition出版之前几乎没有人知道枚举单例,在老代码中无法找到枚举单例的参照
    • 目前Java Web开发对象管理都交给DI容器了,DI容器可以保证项目中对象是单例,大家直接手撸单例的机会也变少了

    单例模式非常简单,如果要学院派一点,深究单例的注意点的话,需要注意的是如下几点:

    • 获取实例是否线程安全
    • 是否需要延迟加载
    • 是否反射安全(enum单例由JVM保证,其他单例可以通过在构造函数中添加检测代码保证)
    • 是否序列化与反序列化安全 (enum单例由JVM保证)

    我们先从一个反例开始,一起看一下单例模式在Java中是如何演进的。

    教学版(错误示例)

    @NotThreadSafe
    public final class Singleton {
        private static Singleton singleton = null; // static 变量保证类内唯一
        private Singleton() { // private 构造函数防止外部调用
            System.out.println("create a Singleton instance!");
        }
        public static Singleton getInstance() {
            if (singleton== null) { // 存在多线程问题
                singleton= new Singleton();
            }
            return singleton;
        }
    }
    

    假定程序的运行环境是单线程的,这段代码如果用做单例模式的教学,单例的主要思想还是描述清楚了。但是真实的软件运行环境是很恶劣的,生产环境还是不可以使用这样的代码的,一旦多个线程在同一时间点调用getInstance()方法,将创建多个Singleton实例!

    测试代码如下

    public class SingletonTest {
    
        @Test
        public void test() {
            int threadsNum = 4;
            long currentTime = System.currentTimeMillis();
            ThreadPoolExecutor executor = buildFixedThreadPool(threadsNum);
            for (int i = 0; i < threadsNum; i++) {
                executor.execute(() -> {
                    keepCheckingUntil(currentTime + 100);
                    Singleton.getInstance();
                });
            }
            executor.shutdown();
            waitExecutorTermination(executor);
        }
    
        private void waitExecutorTermination(ThreadPoolExecutor executor) {
            try {
                executor.awaitTermination(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                System.out.println(e.getStackTrace());
            }
        }
    
        private ThreadPoolExecutor buildFixedThreadPool(int poolSize) {
            return new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue(poolSize));
        }
    
        private void keepCheckingUntil(long timestamp) {
            while(System.currentTimeMillis() < timestamp);
        }
    }
    

    运行测试得到如下运行结果

    create a Singleton instance!
    create a Singleton instance!
    create a Singleton instance!
    create a Singleton instance!
    

    在同一时间点创建了4个实例

    线程安全版(不推荐)

    public final class Singleton {
        private static Singleton singleton = null; // static 变量保证类内唯一
        private Singleton() { // private 构造函数防止外部调用
            System.out.println("create a Singleton instance!");
        }
        public static synchronized Singleton getInstance() { //存在性能问题
            if (singleton== null) {
                singleton= new Singleton();
            }
            return singleton;
        }
    }
    

    这个版本直接在方法上添加了synchronized关键字,再运行上面的单元测试能够看到正确的日志输出,只有一个实例被创建。但是直接在方法上加synchronized关键字实在太粗暴了,这样在程序整个运行期间内,都要通过获得锁的方式获取Singleton实例,在高并发的场景下调用getInstance()会使得线程串行执行,并且频繁的获得锁与释放锁操作也很多余。在Singleton实例已经被创建之后完全没有必要加锁了,直接返回创建好的实例就好了,于是又衍生出了只在创建Singleton实例的时候才进行加锁的方式

    双重检测版本(不推荐)

    public final class Singleton {
    
        private static Singleton singleton = null;
    
        private Singleton() {
            System.out.println("create a Singleton instance!");
        }
    
        public static Singleton getInstance() {
            if (singleton == null) { // 第一次检测,实例创建好之后直接返回
                synchronized (Singleton.class) { //最小力度加锁
                    if (singleton == null) { // 第二次检测,防止多线程环境多次创建
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    双重检测版本的思想就是将锁的范围缩小,只在第一次创建实例的时候进行加锁,来获得更好的性能。有没有觉得上面的代码太繁琐了,我只想要一个单例但是却要写这么多代码,有没有简单一点的方式?

    静态常量(推荐)

    public final class Singleton {
    
        private Singleton() {
            System.out.println("create a Singleton instance!");
        }
    
        private static final Singleton INSTANCE = new Singleton();
    
        public static Singleton getInstance() {
            return INSTANCE;
        }
    }
    

    这种方式因为使用了静态常量,这个类一旦被使用,这个类就会被JVM载入内存,并且在特定条件下会对这个类进行初始化,即使不调用getInstance方法,INSTANCE实例也已经被初始化了,是一个比较推荐的方式,通过提前初始化的方式,保证了getInstance方法是线程安全的,在项目代码中可能会找到在大量的这种单例。这里引出了一个问题,JVM什么时候会对对象进行初始化?这里引用一下另外一片博客的内容,详细内容也可以参考《深入理解Java虚拟机》

    什么情况下需要开始类加载过程的第一个阶段:"加载"。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。
    1、创建类的实例
    2、访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
    3、访问类的静态方法
    4、反射如(Class.forName("my.xyz.Test"))
    5、当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
    6、虚拟机启动时,定义了main()方法的那个类先初始化
    以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用”
    接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。
    被动引用例子
    1、子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
    2、通过数组定义来引用类,不会触发类的初始化
    3、 访问类的常量,不会初始化类
    

    那么有没有只有在调用getInstance方法的时候才初始化我们的单例实例的简单方法呢?答案就是内部静态类和枚举单例

    内部静态类(推荐)

    public final class Singleton {
    
        private Singleton() {
            System.out.println("create a Singleton instance!");
        }
    
        static public Singleton getInstance() {
            return HelperHolder.INSTANCE;
        }
    
        private static class HelperHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    }
    

    如果非要做到懒加载,在只在调用getInstance方法的时候才创建单例实例,内部静态类方式可以满足。因为内部静态类其实也是一个类,只不过这个类的访问权限受限。JVM加载Singleton类也不会导致HelperHolder类被加载,HelperHolder类只有在getInstance方法被调用时才会被载入内存。

    关于懒加载

    懒加载将对象初始化工作推迟到程序运行期间,好处是缩短了程序的启动时间。弊端也很明显,一旦对象无法初始化,这种异常状态怎么处理?此时程序已经处于运行状态了,难以人工干预,一旦没有正确处理异常,生产环境将会产生大量的错误。如果不使用懒加载,程序启动时就出现异常,阻止程序启动,防止生产环境出现问题,早发现早治疗!

    结束语

    关于Java单例的最佳选择当然是枚举了,JVM的机制就已经保证了枚举单例的安全性。静态常量与内部静态类也是可以使用的,因为在绝大多数日常开发中,对于单例安全要求没有那么苛刻,并且老代码里面应该随处可见静态常量与内部静态类的单例设计。

    但是枚举单例简直找不出什么弊端,感觉大家可以在你今后项目中尝试使用枚举作为单例,简单高效,又不容易出错。

    参考文档

    1. CoolShell.深入浅出单实例SINGLETON设计模式
    2. Java类加载机制
    3. 枚举类实现原理
    4. Java-Design-Pattern
  • 相关阅读:
    牛客小白月赛16E
    洛谷P1309 瑞士轮
    洛谷P1781 宇宙总统
    洛谷P1068 分数线划定
    洛谷P1059 明明的随机数(桶排思想)
    洛谷P1177 【模板】快速排序 (归并排序)
    Python基础-----sys模块
    Python基础-----模块导入注意事项
    Python基础-----os模块
    Python基础-----random随机模块(验证码)
  • 原文地址:https://www.cnblogs.com/migoo/p/12637587.html
Copyright © 2020-2023  润新知