美文网首页
浅析设计模式-单例模式

浅析设计模式-单例模式

作者: RunAlgorithm | 来源:发表于2017-08-21 23:19 被阅读112次

    定义

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

    现实世界模型:

    比如古代一个国家只能有一个皇帝

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

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

    简单设计

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

    单例模式-类图.png

    要正确的写好单例,需要关注两点:

    • 多线程下的初始化是否是线程安全的
    • 性能如何

    主要分两种方法,懒汉和饿汉,区别在初始化唯一实例的时机。懒汉延迟初始化,饿汉在类加载的时候就初始化了

    同时 Java 还有一个特殊的实现方式,使用枚举实现

    懒汉

    延迟加载单例,只有第一次使用的时候才进行实例化

    懒汉需要做一些线程安全的处理

    如果没有进行多线程处理,比如:

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

    假设有两个线程,A 线程和 B 线程,两个线程同时调用 getInstance

    当 A 线程执行 instance = new Singleton(); 语句还没结束时,instance 还为 null

    这时候 B 线程进入语句 if (instance == null) ,条件成立,也进入 instance = new Singleton();

    这时候,这个类就会产生两个实例,都被外界拿去使用了

    保证延迟加载并且线程安全的做法主要有这三种:

    方法加同步

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

    直接在 getInstance() 方法加同步,线程安全

    缺点是调用 getInstance 比较密集的场景,同步方法频繁调用,性能低

    静态内部类

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

    利用 Java 的类加载机制,来延迟初始化对象实例。类被主动调用的时候,如果没有加载会执行加载

    我们这里使用的是静态内部类,在没有被访问前,还没有进行加载。在使用 getInstance 方法后,该静态内部类被主动调用,于是开始了对这个类的类加载过程。

    因为 Java 的类加载过程是同步的,包括类静态成员的初始化也是同步的,这个做法无需再单独加锁

    ClassLoader 加载类的代码:

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

    可见有对加载进行同步处理

    双重检查锁定

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

    双重检查锁定和同步静态方法对比,把锁的粒度降低,所以只有在判断当前实例为空,才会进入被锁住的代码。这样子提高了使用的性能,保证在实例存在的情况下,不会因为互斥锁导致多个线程阻塞等待的现象

    同时 volatile 关键字还确保了,虚拟机进行指令优化的时候,不会进行重排序导致对象延迟初始化

    volatile 关键字的主要功能有三:

    • 防止指令重排序
    • 保证内存可见性
    • 保证 long 或者 double 等基本类型的原子操作

    如果没有加 volatile 关键字,由于虚拟机的指令排序优化,会把对象创建延迟到使用的时候。会出现 singleton 返回不会 null 的时候,但是还没有执行初始化对象实例,当另一个线程拿到对象实例,即使不为 null,但实际还是未初始化的对象

    饿汉

    在类加载后就实例化出一个静态对象出来,

    静态工厂方法

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

    饿汉始终是线程安全的

    他的缺点后面即使没有使用单例,也会一直存在,某些场景下会耗费内存(比如某个进程一直都没有使用到那个单例,而这个单例持有大量内存,造成不必要的浪费)

    枚举

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

    创建枚举默认是线程安全的,同时不需要担心反序列化导致重新创建新对象

    应用

    单例模式应用范围很广,可以在这里见到

    控制实例的访问,需要访问一定在同一实例上进行的,比如 HttpClient

    控制资源的使用,和线程池或者对象池配合使用,资源类型的单例,避免创建多个池浪费资源,节约内存

    控制对象的创建,不需要多次重复创建,和工厂模式配合使用,一些工厂只需要进程中只保持一份,

    优缺点

    优势:

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

    缺点:

    • 如果职责过多,会导致单例类违背单一职责原则(拆分出其他模块来执行,单例做一个入口)
    • 一些不必要的内存引用,会造成内存溢出(要有内存释放机制)
    • 一些错误的引用,比如引用了不再使用的对象(典型的有对 Activity 的引用),会造成内存泄漏

    使用注意

    单例模式,实例一旦初始化,就会在内存中一直存在,要注意几点

    • 不能滥用,要确定该实例是需要在整个进程中长期使用的
    • 如果是资源单例的话,要考虑适当地释放一些很少使用的资源,避免页面增长造成内存泄漏
    • 实例的成员变量,不要持有生命周期短的对象,如果持有要记得释放,否则会导致内存一直无法回收而造成内存泄漏
    • 如果一个类是单例设计,不要用反射去生成它的实例,反射直接破坏对这个类的设计
    • 使用懒汉式要注意线程安全

    相关文章

      网友评论

          本文标题:浅析设计模式-单例模式

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