美文网首页JavaJava 杂谈Java服务器端编程
唯一实例与中心化——单例模式

唯一实例与中心化——单例模式

作者: RunAlgorithm | 来源:发表于2019-05-13 23:09 被阅读0次

    1. 定义

    单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

    一句话总结,就是唯一实例,中心化

    现实世界的模型:

    • 日本只有一个天皇。
    • 一个国家只有一个央行。

    单例模式需要满足以下的特点:

    • 单例类全局只有一个实例。
    • 单例类必须自己创建自己的唯一实例。
    • 单例类必须给所有其他对象提供这一个实例。

    2. 设计

    单例类的类图可以简单表示为: 单例模式

    如果正确地设计单列,需要关注两个点:

    • 线程安全。
    • 性能。

    因为单例类需要自己实例化自身,并且要确保在多线程环境下不会产生多个实例,而且并发下性能到达最优。

    根据实例化的时机,分为两种:

    • 饿汉模式,类加载的时候就初始化。

    • 懒汉模式,延迟初始化。

    2.1. 懒汉:错误方式

    为了实现懒汉,延迟初始化单例,我们把实例化的过程推迟到第一次访问单例上。于是就有了这样的代码

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

    看上去很美好,实则有一个很大的隐患,多线程调用 getInstance 方法会产生多实例。

    简单分析一下,假设 A 线程和 B 线程同时调用

    • A 线程执行 instance = new Singleton(); ,语句还没结束, instance 还为 null
    • 这时候 B 线程进入语句 if (instance == null) ,条件成立,也进入 instance = new Singleton();
    • 两个线程均建立了实例

    为了确保能够延迟初始化,并且做到线程安全,下面会开始介绍

    2.2. 懒汉:方法直接加同步(不要用)

    最简单粗暴的方式,获取实例的方法直接上锁:

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

    确实线程安全了。

    但是 synchronized 是互斥锁,悲观锁,getInstance 被调用频繁的情况下性能低下。

    这样吧,我们把锁的粒度

    2.3. 懒汉:双重检查锁定(推荐,记得 volatile)

    双重检查锁定,本质是对锁的一个粒度优化。

    public class Singleton {  
        private volatile static Singleton singleton;  
        private Singleton (){}  
        public static Singleton getInstance() {  
            if (singleton == null) {  
                synchronized (Singleton.class) {  
                    if (singleton == null) {  
                        singleton = new Singleton();  
                    }  
                }  
            }  
            return singleton;  
        }  
    } 
    

    只有判断实例为空,才会进入同步代码。实例存在的话就直接返回了。

    所以只有在创建的那一刹那,并且有多线程并发,会有一个互斥等待的过程。

    因为进入同步代码,有可能其他线程已经实例化完毕,所以还要再检查一下是否已经实例化(判空),这就是双重检查。

    因为 JVM 的指令优化,指令重排序现象会导致对象延迟初始化,所以其他线程读到实例不为空的时候,可能还没初始化。

    所以需要加个 volatile 关键字,禁止重排序优化。

    2.4. 懒汉:静态内部类(非常推荐)

    这是一个比较机智的做法,利用 JVM 类加载机制,来延迟初始化对象实例

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

    什么时候会进行类加载?

    这里的静态内部类,外部的 Singleton 加载的时候并不会引起它的加载。因为虽然是它的内部静态类,但编译成字节码文件后是两个单独的类。

    在调用 getInstance 方法后,静态内部类被主动调用,触发类加载流程。

    而类加载流程是天然同步的,我们可以从源码上看到:

     protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
     {
        synchronized (getClassLoadingLock(name)) {
            ...
            return c;
        }
    }
    

    我们也就不需要再进行加锁了。

    2.5. 饿汉:静态工厂方法

    在单例类加载的时候,马上实例化单例对象。

    public class Singleton {  
        private Singleton() {}  
        private static final Singleton singleton = new Singleton();  
        public static Singleton getInstance() {  
            return singleton;  
        }  
    }  
    

    始终是线程安全的。

    缺点是,及时没有调用该单例,也会在类使用的时候一开始就创建好了,在一些对性能要求高的场景会消耗性能。(这个在客户端场景比较常见)

    2.6. 枚举实现

    public enum Singleton {
        INSTANCE;
        private Singleton() {
        }
    }
    

    创建枚举默认是线程安全的

    3. 应用

    什么时候需要使用单例?

    控制实例的访问,所有的访问必须在单一实例上进行。

    控制资源的使用,和线程池、连接池等配合使用,资源型单例,避免创建多个池导致资源的浪费。

    控制对象的创建,不需要重复创建的对象,和工厂模式配合,比如实现工厂实例的唯一。

    在客户端环境,比如 Android 开发,因为对应用启动的性能要求高,不希望应用一加载马上进行单例实例化,所以对懒汉模式应用较多。

    3.1. JDK:Runtime

    Runtime 可以获取 JVM 运行环境的信息。

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
        
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
        
        ...
    }
    

    4. 特点

    4.1. 优势

    • 中心化:控制所有的访问在唯一实例上进行
    • 性能优化:避免频繁创建和销毁对象带来的性能损耗
    • 内存优化:避免可共享资源的重复创建

    4.2. 缺点

    • 职责膨胀:如果承担的职责过多,违背单一职责原则

      优化思路

      把职责再重新剥离出去,或者与其他设计模式一起使用,单例仅作为入口

    • 内存泄漏:如果成员变量引用了本来该释放的对象,引起泄漏,进而导致 OOM(内存溢出)

      优化思路

      第一,注意引用对象的生命周期

      第二,如果确定要引用,需要有良好的内存释放机制。比如该成员变量为缓存池,考虑使用弱引用或者软引用,在虚拟机垃圾回收阶段或者内存紧张的时候进行对象的回收

    4.3. 注意事项

    • 不要滥用:实例和进程生命周期一致,长期不使用白白占用内存
    • 不要反射:发射生成实例,会破坏单例的设计。(之前有遇到坑,继承的第三方服务反射一个单例类导致不唯一)
    • 注意引用:成员变量尽量不去持有生命周期短的对象,非要持久需要注意释放机制
    • 注意性能:根据业务来决定是否需要延迟初始化(用来抉择懒汉还是饿汉)
    • 注意线程安全:使用懒汉模式,注意线程安全避免产生多个实例

    相关文章

      网友评论

        本文标题:唯一实例与中心化——单例模式

        本文链接:https://www.haomeiwen.com/subject/axidaqtx.html