美文网首页Android开发Android开发经验谈Android技术知识
Java设计模式 | 单例模式解析与实战

Java设计模式 | 单例模式解析与实战

作者: 凌川江雪 | 来源:发表于2020-04-10 00:55 被阅读0次

    定义

    确保某一个类只有一个实例
    而且自行实例化并向整个系统提供这个实例

    使用场景

    确保某个类有且只有一个对象的场景,
    避免产生多个对象消耗过多的资源,
    或者
    某种类型的对象只应该有且只有一个。
    例如,
    创建一个对象需要消耗的资源过多,
    如要访问IO和数据库等资源,这时就要考虑使用单例模式。

    单例模式UML类图

    • 角色:

    (1)Client——高层客户端;
    (2)Singleton——单例类。

    • 实现单例模式的关键点:
      (1)构造函数不对外开放,一般为Private
      (2)通过一个静态方法或者枚举返回单例类对象
      (3)确保单例类的对象有且只有一个,尤其是在多线程环境下;
      (4)确保单例类对象反序列化时不会重新构建对象

    • 通过将单例类的构造函数私有化
      使得客户端代码不能通过 new 的形式手动构造单例类的对象

    • 单例类会暴露一个公有静态方法
      客户端需要调用这个静态方法获取到单例类唯一对象

    • 在获取这个单例对象的过程中需要确保线程安全
      即在多线程环境下构造单例类的对象也是有且只有一个
      这也是实现的难点

    重点,注意单例模式中 volatile的重要性

    单例的几种实现方式

    1. 饿汉模式

    声明一个静态类对象,在声明时就己经初始化
    用户调用类对象get方法时,可以直接拿去用;
    【一声明就初始化,所谓“饿”】

    如下,
    CEO类使用了饿汉单例模式;

    /**
     * 普通员工
     */
    class Staff {
        public void work() {
            // 干活
        }
    }
    // 副总裁
    class VP extends Staff {
        @Override
        public void work() {
            // 管理下面的经理
        }
    }
    // CEO, 饿汉单例模式
    class CEO extends Staff {
        private static final CEO mCeo = new CEO();
    
        // 构造函数私有
        private CEO() {
        }
    
        // 公有的静态函数,对外暴露获取单例对象的接口
        public static CEO getCeo() {
            return mCeo;
        }
        @Override
        public void work() {
            // 管理VP
        }
    
    }
    // 公司类
    class Company {
        private List<Staff> allPersons = new ArrayList<Staff>();
    
        public void addStaff(Staff per) {
            allPersons.add(per);
        }
    
        public void showAllStaffs() {
            for (Staff per : allPersons) {
                System.out.println("Obj : " + per.toString());
            }
        }
    }
    public class Test {
        public static void main(String[] args) {
            Company cp = new Company();
            // CEO对象只能通过getCeo函数获取
            Staff ceo1 = CEO.getCeo();
            Staff ceo2 = CEO.getCeo();
            cp.addStaff(ceo1);
            cp.addStaff(ceo2);
            // 通过new创建VP对象
            Staff vp1 = new VP();
            Staff vp2 = new VP();
            // 通过new创建Staff对象
            Staff staff1 = new Staff();
            Staff staff2 = new Staff();
            Staff staff3 = new Staff();
    
            cp.addStaff(vp1);
            cp.addStaff(vp2);
            cp.addStaff(staff1);
            cp.addStaff(staff2);
            cp.addStaff(staff3);
    
            cp.showAllStaffs();
        }
    }
    

    2. 懒汉模式

    • 懒汉模式是声明一个静态对象,
      并且在用户第一次调用getInstance进行初始化
      【“拖延”,等到调用才初始化,所谓“懒”!】
        public class Singleton {
            private volatile static Singleton instance;
            private Singleton () {}
            public static synchronized Singleton getInstance() {
                if (instance == null) {
                    instance = new Singleton ();
                }
                return instance;
            }
        }
    
    • getInstance()中添加了 synchronized 关键字,
      也就是 getInstance()是一个同步方法,
      即上面所说的在多线程情况下保证单例对象唯一性的手段。

    • 只不过这里可能有一个问题,
      即使instance己经被初始化(第一次调用时就会被初始化instance),
      每次调用getInstance方法都会进行同步
      这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

    • 优点:单例只有在使用时才会被实例化,在一定程度上节约了资源

    • 缺点:第一次加载时需要及时进行实例化,反应稍慢,
      最大的问题是每次调用 getInstance都进行同步,造成不必要的同步开销

    这种模式一般不建议使用!!!!!!!!!!

    3. DoubleCheckLock(DCL)实现单例【双重校验锁】

    • 优点:
      资源利用率高,
      第一次执行getInstance()时单例对象才会被实例化,效率高。
      既能够在需要时才初始化单例,
      又能够保证线程安全
      且单例对象初始化后每次调用getInstance()不进行同步锁
      减少不必要的同步开销
    • 缺点:第一次加载时反应稍慢,
      也由于 Java 内存模型的原因偶尔会失败。
      在高并发环境下也有一定的缺陷,虽然发生概率很小。
    public class Singleton {
            private volatile static Singleton sInstance = null;
            private Singleton() {
            }
            public void doSomething() {
                System.out.println("do sth.");
            }
            public static Singleton getInstance() {
                if (mInstance == null) {
                    synchronized (Singleton.class) {
                        if (mInstance == null) {
                            sInstance = new Singleton();
                        }
                    }
                }
                return sInstance;
            }
    
    • static 保证单例;
      volatile 禁止重排序;
      getInstance() 用来获取实例;
      synchronized 保证原子性、可见性、线程安全;

    • 亮点:getInstance()方法中对instance进行了两次判空:
      第一层判断主要是为了避免不必要的同步【有实例则直接返回,没必要同步】,
      第二层的判断则是为了在null的情况创建实例
      【可能第一层与第二层判断中途有其他线程初始化完成了单例,
      单例不为null,就不用创建了】:

      假设线程A和线程B先后访问了getInstance();
      线程A执行到sInstance = new Singleton()语句,
      这里看起来是一句代码,但实际上它并不是一个原子操作
      这句代码最终会被编译成多条汇编指令,它大致做了3件事情:
      (1)给Singleton的实例分配内存
      (2)调用Singleton()构造函数初始化成员字段;
      (3)将sInstance对象指向分配的内存空间(此时sInstance就不是null了)。

      但是,由于Java编译器允许处理器乱序执行
      以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、
      寄存器到主内存回写顺序的规定,
      上面的第二和第三的顺序是无法保证的。【指令重排序】

      即,执行顺序可能是1-2-3也可能是1-3-2。
      如果是后者,并且在3执行完毕2未执行之前,被切换到线程B上,
      这时候sInstance因为己经在线程A内执行过了第三点,
      sInstance己经是非空了,
      所以,
      线程B通过getInstance()直接取走sInstance
      再使用时就会出错,这就是DCL失效问题
      而且这种难以跟踪难以重现的错误很可能会隐藏很久。
      在JDK1.5之后,SUN官方己经注意到这种问题,
      调整了JVM,具体化了volatile关键字,
      因此,
      如果JDK是1.5或之后的版本,
      只需要将sInstance的定义改成private volatile static Singleton sInstance = null就可以保证sInstance对象每次都是从主内存中读取
      就可以使用DCL的写法来完成单例模式
      当然,volatile 或多或少也会影响到性能
      但考虑到程序的正确性,牺牲这点性能还是值得的。^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    • DCL 模式是使用最多的单例实现方式!!!!
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      它能够在需要时才实例化单例对象,
      并且能够在绝大多数场景下保证单例对象的唯一性
      除非代码在并发场景比较复杂或者低于JDK 6版本下使用,
      否则,这种方式一般能够满足需求。

    4. 静态内部类单例模式

    • DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,
      但是,它还是在某些情况下出现失效的问题。
      就是刚说的双重检查锁定(DCL)失效
    • 在《Java 并发编程实践》一书的最后谈到了这个问题,
      并指出这种“优化”是丑陋的,不赞成使用
      而建议使用如下的代码替代
    public class Singleton {
            private Singleton() { }
            public static Singleton getInstance () {
                return SingletonHolder.sInstance;
            }
            /**
             * 静态内部类
             */
            private static class SingletonHolder {
                private static final Singleton sInstance = new Singleton();
            }
        }
    
    • 当第一次加载Singleton类时并不会初始化sInstance
      只有在第一次调用SingletongetInstance()sInstance才会被初始化!!!
      因此,
      第一次调用getInstance()会导致虚拟机加载SingletonHolder类
      这种方式不仅能够确保线程安全
      也能够保证单例对象的唯一性,同时也延迟了单例的实例化
      所以这是推荐使用的单例模式实现方式。

    5. 枚举单例

    除了以上几种方式,还有更简单的实现方式——枚举!:

    public enum SingletonEnum {
            INSTANCE;
            public void doSomething() {
                System.out.println("do sth.");
            }
        }
    
    • 优点突出:写法简单;

      枚举在Java中与普通的类是一样的,
      不仅能够有字段,还能够有自己的方法。

      最重要的是默认枚举实例创建线程安全的,
      并且在任何情况下它都是一个单例

      在上述的几种单例模式实现中,
      在一个情况下它们会出现重新创建对象的情况,那就是反序列化

      通过序列化可以将一个单例实例对象写到磁盘
      然后再读回来,从而有效地获得一个实例

      即使构造函数私有的,
      反序列化时依然可以通过特殊的途径去创建类的一个新的实例
      相当于调用该类的构造函数
      反序列化操作提供了一个很特别的钩子函数
      类中具有一个私有的、被实例化的方法readResolve()
      这个方法可以让开发人员控制对象的反序列化
      例如,
      上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,
      那么必须加入如下方法:
    private Object readResolve() throws ObjectStreamException {
        return sInstance;
    }
    

    即在readResolve()中将sInstance对象返回,
    而不是默认的重新生成一个新的对象。
    而对于枚举,并不存在这个问题,
    因为即使反序列化它也不会重新生成新的实例。







    参考:

    • 《Android源码设计模式解析与实战》

    相关文章

      网友评论

        本文标题:Java设计模式 | 单例模式解析与实战

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