• 单例模式


    介绍

      单例模式(Singleton Pattern)可能是设计模式中用的最多的模式,单例模式非常简单同时代码也比较短方便手写代码,所以也是面试中经常会问到的设计模式。单例模式它是一种对象创建模式,它用于确保系统中一个类只产生一个实例。例如:一个系统使用AppConfig对象读取诸如xml和properties配置文件,而当系统运行时,能够存在多个AppConfig对象,而每一个AppConfig对象中都封装着配置文件,这样非常浪费内存资源,因此系统内部只需一个该配置对象,这里就需要使用单例模式了。

      本文针结合网上的资料对单例模式的常有的写法进行整理,希望对你有一定帮助。

    最直接的单例模式

      当面试题被问到这一题的时候,很多人的根据直觉可能写出下面的代码。

    public class Singleton01 {
        private static Singleton01 instance;
        private Singleton01() {}
        public static Singleton01 getInstance() {
            if (instance == null) {
                instance = new Singleton01();
            }
            return instance;
        }
    }

      上面的代码简单明了,是很多人的最终答案,上面的代码还使用懒加载模式(就是需要使用实例对象时,才创建对象)。但有个明显的缺陷,就是只能在单线程中使用,在多线程时,会创建多个实例,造成内存泄漏。

    线程安全的单例模式

      遇到多线程的问题,自然而然地想到加锁,对获取实例方法加锁后,多线程情况下就可以保证线程安全。

    public class Singleton02 {
        private static Singleton02 instance;
        private Singleton02() { }
        public static synchronized Singleton02 getInstance() {
            if (instance == null) {
                instance = new Singleton02();
            }
            return instance;
        }
    }

      但上面的代码,虽然是线程安全,但是每一次使用getInstance()获取实例时,都必须加同步锁,加锁是很耗时的操作,在没有必要的时候我们应该尽量避免。

    双重检验锁

      对于上面的效率不高的问题,我们可以使用双重检验锁(Double-checked locking)来进行解决,我们应该在实例还没有创建之前需要加锁操作,以保证只有一个实例,当实例创建之后,就不再需要做加锁操作,在此过程中,会两次检查instance==null,称为double checked,其中一次在同步块外,一次在同步块内,至于为什么在同步块内还要检验一次,是因为可能多个线程都进行if代码中,如不进行检验,就可能创建多个实例。

    public class Singleton03 {
        private static Singleton03 instance;
        private Singleton03() { }
        public static synchronized Singleton03 getInstance() {
            if (instance == null) {         // Single check
                synchronized (Singleton03.class) {
                    if (instance == null) {      // double check
                        instance = new Singleton03();
                    }
                }
            }
            return instance;
        }
    }

      上面的代码看上去非常完美,但是还存在问题,主要在于instance = new Singleton03();这一句,这个语句并不是一个原子操作,它可以分解为下面三步:

      1. 给对象引用instance分配内存 ;

      2. 调用Singleton03的构造函数来初始化对象成员变量;

        3. 将生成的实例对象的内存地址赋值给对象引用instance。

       然而上面的3个步骤并不是最终的执行顺序,JVM中即时编译器中存在指令重排序的优化,最后的执行顺序,可能是1-2-3,也可能是1-3-2,当为第二种情况时,线程一刚执行第3步,线程二直接返回instance,然后instance这个对象引用已经指向堆内存,但堆内存中却没有初始化,于是报错,对于这个问题将instance声明为volatile即可。

    public class Singleton03 {
        private volatile static Singleton03 instance;
        private Singleton03() { }
        public static synchronized Singleton03 getInstance() {
            if (instance == null) {         // Single check
                synchronized (Singleton03.class) {
                    if (instance == null) {      // double check
                        instance = new Singleton03();
                    }
                }
            }
            return instance;
        }
    }

    饿汉式

      上面的单例模式都是使用懒加载的方式,可以称为懒汉式,这里介绍的是饿汉式,顾名思义,实例对象在使用之间就创建好了,直接用就可以了。

    public class Singleton04 {
        private final static Singleton04 INSTANCE = new Singleton04();
        private Singleton04() { }
    
        public static Singleton04 getInstance() {
            return INSTANCE;
        }
    }

      上面的代码中当classloader加载Singleton04类时就创建了该实例,之后可以直接使用,这样一定是线程安全的,但这样的话,我们将实例的创建委托给类加载器,可能与我们要求的行为可能不太一致,我们可能希望在使用getInstance方法才创建单例对象。

    静态内部类

      为了解决上面的问题,我们可以使用静态内部类的方法,既可以在第一次使用getInstance时创建单例对象,又可以保证同步安全。

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

      上面的代码,SingletonHolder为私有静态类,只能通过getInstance访问,所以是懒加载的,同时获取实例时无需同步,没有性能上的损失。

    枚举

      最简单的方式居然是用枚举,让人意想不到。

    public enum Singleton06 {
        INSTANCE;
        private Singleton06() {
        }
        public static Singleton06 getInstance() {
            return INSTANCE;
        }
    }

      使用枚举创建实例默认是线程安全的,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。上面的getInstance方法可以不要,可以直接使用Singleton06.INSTANCE进行使用。

    总结

      单例模式的特点

        1、类的实例对象只有一个;

          2、可以通过类自身的方法创建实例对象 ;

          3、系统使用的对象为同一个对象。

      单例模式的使用方法小小的单例模式同样有很多种不同的写法,但根据我的经验推荐大家使用双重检验锁的方式,或直接使用饿汉式的方式,使用枚举的方式实际工作中感觉很少使用。

      单例模式的注意事项:单例模式它是有范围的,它只局限于类加载器(ClassLoader)中能保证实例唯一,当有不同的类加载器时,会出现不同的类加载器装载同一个类,从而产生多个实例,所以,应该尽量保证多个类加载器不会装载同一个单例类。

  • 相关阅读:
    24.Spring-Boot-Actuator与Spring-Security整合应用
    Spring Boot 和 Spring 到底有啥区别?用了这么久,你知道吗?
    一文,5 分钟搞明白 MySQL 是如何利用索引的!
    大厂面试必问的Spring全家桶 4 大开源框架,思维脑图全总结,终于出来了
    这些SQL错误用法,如果经常犯,说明你的水平还很low...
    新技能 MyBatis 千万数据表,快速分页!
    牛逼!在IDEA里搞Spring Boot Mybatis反向工程,太爽咯~
    有了 HTTP 协议,为什么还要 RPC 协议,两者有什么区别?
    把 Spring Cloud 给拆了!详解每个组件的作用,值得收藏!
    27个阿里 Java 开源项目,值得收藏!
  • 原文地址:https://www.cnblogs.com/codingexperience/p/5796852.html
Copyright © 2020-2023  润新知