zoukankan      html  css  js  c++  java
  • 单例模式(Singleton Pattern)到底该怎么写

    单例模式(Singleton Pattern)

    当我们有这样的需求:某一些类应该只存在一个实例 的时候,我们就可以用单例模式来应对.

    单例模式:确保一个类只有一个实例,并提供一个全局访问点.

    单例模式是所有设计模式中最简单的一个,也是大部分人最早知道的一个设计模式.

    但是即使是最简单的,也有很多可以推敲的细节,要做得对也不简单.

    经典的单例

    相信大家一定写过这样类似的单例模式代码:

    public class Singleton {
        private static Singleton ins; //私有化成员变量 (静态是为了在静态方法中引用) , 私有化不允许在类外修改类的实例对象引用
        private Singleton() {} //私有化构造方法,仅在本类中可见,从而将实例化该对象的范围缩小到了该类中
        public static Singleton getIns() {
            if (null == ins) { //判断该变量如果没有实例化,则 执行一次new创建
                ins = new Singleton();
            }
            return ins;
        }
    }

    简单总结一下这样的写法:

    1. 提供一个全局静态的getIns方法,使得易于使用.

    2. 延迟Singleton的实例化,节省资源(所谓的懒汉式).

    3. 缺点是线程不安全.当多个线程同时进入if (null == ins) {}的时候就会创建多个实例.

    OK,接下来我们看来是要解决多线程不安全的问题了.

    多线程安全

    这个时候可能有的人就说了,这个太简单了,加一个synchronized不就结了吗?

    public static synchronized Singleton getIns() {
        if (null == ins) {
            ins = new Singleton();
        }
        return ins;
    }

    确实,增加synchronized之后能够迫使每个线程在进入这个方法之前,要先等别的线程离开该方法.也即避免了多个线程同时进入getIns方法.

    诚然这个能解决问题,但是我们知道synchronized是非常耗性能的.
    更何况:
    我们只需要在第一次执行这个方法的时候同步,也就是说当ins实例化后,我们不再需要同步了
    而如果我们加了synchronized,那么实例化后的每次调用getIns都是一种多余的消耗操作,是累赘

    那么问题来了,如何改善?
    如何确保单例,而又不影响性能?

    性能进阶

    接下去介绍一种更优秀的,线程安全的单例写法---双重检查锁模式(double check locking pattern)

    public class DoubleCheck {
        private volatile static DoubleCheck ins;
        private DoubleCheck() {}
        public static DoubleCheck getIns() {
            if (null==ins){ //检查
                synchronized (DoubleCheck.class){
                    if (null == ins) { //又检查一次
                        ins = new DoubleCheck();
                    }
                }
            }
            return ins;
        }
    }

    注意这里的ins用了volatile关键字来修饰,为什么呢?
    因为执行ins = new DoubleCheck()做了很多事情:

    1. 给ins分配内存

    2. 调用构造函数来初始化成员变量(可能会很久)

    3. 将ins对象指向分配的内存空间(执行完这步 ins才不为null)

    上面的操作并不是原子操作,而jvm也可能重排序指令,导致第二三两步的执行顺序可能会被打乱,当第3步先于第2步完成,那么会导致有线程拿到了初始化未完毕的ins,那么就会错误,而这里利用了volatile的禁止指令重排序优化特性,用来解决这个问题.

    注:volatile 在java 5 后才有效,原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,当然现在不需要担心这个啦!

    小结

    双重检查非常适用于高并发,我们熟知的开源库Eventbus,ImageLoader等都是用的双重检查锁方式实现单例

    不过它,写起来稍微复杂了些,有没有简单点的呢?
    答案是:有!

    饿汉式

    直接上代码吧

    public class Early {
        private static final Early ins = new Early();
        public static Early getIns() {
            return ins;
        }
    }

    饿汉式的原理其实是基于classloder机制来避免了多线程的同步问题

    饿汉式与之前提到的懒汉式不同,它在我们调用getIns之前就实例化了(在类加载的时候就实例化了),所以不是一个懒加载,这样就有几个缺点:

    1. 延长了类加载时间

    2. 如果没用到这个类,就浪费了资源(加载了但是没用它)

    3. 不能传递参数(很显然适用的场景会减少)

    静态内部类

    静态内部类原理同上,另外虽然它看上去有点饿汉式,但是与之前的饿汉有点不同,它在类Singleton加载完毕后并没有实例化,

    而是当调用getIns去加载Holder的时候才会实例化,静态内部类的方式把实例化延迟到了内部类的加载中去了!

    所以它比饿汉式更优秀!(偷偷告诉你Effective Java中也推荐这个方式)

    例子:

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

    枚举

    最后介绍一种,也是我在 EffectiveJava中看到的,但是在开发中我重来没看到过!

    public enum Singleton{
        INSTANCE;
    }

    优点:简单,线程安全,防反序列化.
    本人不太了解,也没用过,所以不敢乱说了~~~
    值得一提的是 EffectiveJava 也提倡这个方式

    这样真的就能保证单例是个单例了吗?

    我们采用了很多方式来保证实例的唯一性,但是真的够了吗?
    如果采用反射呢?
    如果使用多个ClassLoder呢?
    那是不是会实例化多个了呢?
    思考一下吧~

    总结

    单例的实现方式有好多种,实际开发中看需求而定,个人比较推荐双重检查锁和静态内部类,这两个我比较常用,其他的说实话我一般不用.
    同时也可以看到单例也有很多需要学习以及思考的地方,完全弄懂也不容易~

  • 相关阅读:
    sql注入-原理&防御
    vulnhub靶场之AI-WEB1.0渗透记录
    python -m http.server 搭建一个简易web下载服务器
    渗透测试工具-sqlmap
    OSI七层模型
    LAXCUS大数据操作系统3.03版本发布,欢迎使用试用
    松耦合和紧耦合的架构设计、性能对比
    Laxcus大数据操作系统2.0(5)- 第二章 数据组织
    从AlphaGo谈通用型人工智能设计
    基于深度推理、自编程、大数据的通用人工智能
  • 原文地址:https://www.cnblogs.com/gloryhope/p/10430444.html
Copyright © 2011-2022 走看看