• 设计模式----单例模式


    设计模式之单例模式


    一. 简介

    1. 什么是单例模式?

    单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

    2. 单例模式的应用场景?
    • 网页中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    • 要求生产唯一序列号。
    • 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等
    3. 单例模式的类型?
    • 懒汉式:在真正需要使用对象时才去创建该单例类对象
    • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

    二. 单例模式的几种写法

    1. 饿汉式

    饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。

    // 饿汉式
    class Singleton {
        //1.私有化构造器函数
        private Singleton() {}
        
        //2.创建本类对象并指向本类引用
        private final static  Singleton instance = new Singleton();
    
        //3.提供一个公有的静态方法,返回实例对象
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    优点:

    • 这种写法比较简单,就是在类装载的时候就完成了实例化。避免了线程同步问题。
    • 在类加载的同时已经创建好一个静态对象,调用时反应速度快。
    • 线程安全

    缺点:

    • 来类装载的时候就完成了实例化,没有达到Lazy Loading的效果。如果从始至终未使用过这个实例,则会造成实例的浪费。
    2. 懒汉式

    懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。

    
    public class Singleton {
        
        //1.私有化构造器函数
        private Singleton(){}
        
        //2.先不创建对象
        private static Singleton instance ;
        //3.提供一个公有的静态方法,返回实例对象
        public static Singleton getInstance() {
            //4.如果instance==null 的时候再去创建,否则直接返回
            if (instance == null) {
                instance = new Singleton();
            }
            return instance ;
        }
      }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    缺点:

    • 线程不安全,多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及 往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

    在这里插入图片描述

    3. 懒汉式(线程安全+性能优化

    要想懒汉式线程安全,最容易想到的方法就是加锁。

    
    public class Singleton {
        //1.私有化构造器函数
        private Singleton() {}
    
        //2.先不创建对象
        private static Singleton instance;
    
        //3.提供一个公有的静态方法,返回实例对象
        public static synchronized Singleton getInstance() {
            //4.如果instance==null 的时候再去创建,否则直接返回
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

    性能优化:

    
    public class Singleton {
        //1.私有化构造器函数
        private Singleton() {}
    
        //2.先不创建对象
        private static Singleton instance;
    
        //3.提供一个公有的静态方法,返回实例对象
        public static Singleton getInstance() {
            //4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            if (instance == null) {
                //5.线程A或线程B获得该锁进行初始化
                synchronized (Singleton.class) {
                    if (instance == null) {
                        //6.其中一个线程进入该分支,另外一个线程则不会进入该分支
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排

    4. 使用volatile防止指令重排

    指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

    创建一个对象,在JVM中会经过三步:

    1. 为singleton分配内存空间
    2. 初始化singleton对象
    3. 将singleton指向分配好的内存空间

    在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。

    在这里插入图片描述

    使用volatile关键字可以防止指令重排序,​其原理较为复杂,这篇博客不打算展开,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。

    volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

    最终的代码如下所示:

    public class Singleton {
        //1.私有化构造器函数
        private Singleton() {}
    
        //2.使用volatile关键字修饰的变量
        private static volatile Singleton instance;
    
        //3.提供一个公有的静态方法,返回实例对象
        public static Singleton getInstance() {
            //4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            if (instance == null) {
                //5.线程A或线程B获得该锁进行初始化
                synchronized (Singleton.class) {
                    if (instance == null) {
                        //6.其中一个线程进入该分支,另外一个线程则不会进入该分支
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    5. 登记式/静态内部类

    这种方式能达到双检锁方式一样的功效,并且线程是安全的,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    public class Singleton {
    
        // 1. 私有化构造函数
        private Singleton (){}
    
        // 2. 使用SingletonHolder类装载Singleton类
        private static class SingletonHolder {
            private static final Singleton instance = new Singleton();
        }
        //3.提供一个公有的静态方法,返回装载Singleton的类
        public static final Singleton getInstance() {
            return SingletonHolder.instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。

    想象一下,如果实例化,instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第3 种方式就显得很合理。

    6. 枚举

    这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch
    提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

    示例如下:

    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    7. 粉碎懒汉式单例与饿汉式单例

    无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

    1. 利用反射破坏单例模式
    public static void main(String[] args) {
        // 获取类的显式构造器
        Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
        // 可访问私有构造器
        construct.setAccessible(true); 
        // 利用反射构造新对象
        Singleton instance1= construct.newInstance(); 
        // 通过正常方式获取单例对象
        Singleton instance2= Singleton.getInstance(); 
        System.out.println(instance1== instance2); // false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    利用反射,强制访问类的私有构造器,去创建另一个对象

    1. 利用序列化与反序列化破坏单例模式
    
    public static void main(String[] args) {
        // 创建输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
        // 将单例对象写到文件中
        oos.writeObject(Singleton.getInstance());
        // 从文件中读取单例对象
        File file = new File("Singleton.file");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        // 判断是否是同一个对象
        System.out.println(newInstance == Singleton.getInstance()); // false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址

  • 相关阅读:
    Altium Designer 相同模块的布局布线复用-AD
    移动通信网络规划:频谱划分
    Mybatis-Plus之复查连表查询的设计和实现
    Spring MVC 八 - 内置过滤器
    网络运维的挑战与解决方案:塑造无缝、高效的IT环境
    前端HTML相关知识
    ssm学生成绩管理
    24-数据结构-内部排序-基数排序
    rust学习~slice迭代器
    Oracle-job跑批变慢案例
  • 原文地址:https://blog.csdn.net/qq_44936392/article/details/127870014