zoukankan      html  css  js  c++  java
  • 单例模式Singleton

    概念

    单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。选择单例模式就是为了避免不一致状态。使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection)

    单例对象的类必须保证只有一个实例存在,可以作为对意图实现单例模式的代码进行检验的标准。

    对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于: 
    懒汉式:指全局的单例实例在第一次被使用时构建。 
    饿汉式:指全局的单例实例在类装载时构建。

    核心思想

    • 使用private修改该类构造器,从而将其隐藏起来,避免程序自由创建该类实例
    • 提供一个public方法获取该类实例,且此方法必须使用static修饰(调用之前还不存在对象,因此只能用类调用)
    • 该类必须缓存已经创建的对象,否则该类无法知道是否曾经创建过实例,也就无法保证只创建一个实例。为此,该类需要一个静态属性来保持曾经创建的实例。

    例子

    1.饿汉:全局的单例实例在类装载时构建的实现方式

    饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

    由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够避免许多由多线程引起的问题。不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

    public class HungryBase {
        private static HungryBase instance = new HungryBase();
    
        /**
         * 私有默认构造方法
         */
        private HungryBase() {
        }
    
        /**
         * 静态工厂方法
         */
        public static HungryBase getInstance() {
            return instance;
        }
    }

    JDK例子(java.lang.Runtime):

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
    
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        private Runtime() {}
    }

    2.饿汉---静态代码块

    public class HungryStaticBlock {
        private static HungryStaticBlock instance = null;
        static {
            instance = new HungryStaticBlock();
        }
        /**
         * 私有默认构造方法
         */
        private HungryStaticBlock() {
        }
    
        /**
         * 静态工厂方法
         */
        public static HungryStaticBlock getInstance() {
            return instance;
        }
    }

    3.饿汉---静态类

    这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第1种和第2种方式不同的是(很细微的差别):第1种和第2种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第1和第2种方式就显得很合理。

    public class HungryStaticClass {
        private static class SingletonHolder {
            private static HungryStaticClass instance = new HungryStaticClass();
        }
        /**
         * 私有默认构造方法
         */
        private HungryStaticClass() {
        }
    
        /**
         * 静态工厂方法
         */
        public static HungryStaticClass getInstance() {
            return SingletonHolder.instance;
        }
    }

    4.懒汉:这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
    懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间

    /**
     * 懒汉 线程不安全 lazy loading很明显,在多线程不能正常工作
     */
    public class LazyBase {
        //静态属性用来缓存创建实例
        private static LazyBase instance = null;
    
        //私有构造方法避免程序自由创建实例
        private LazyBase() {
        }
    
        //静态公共方法用于取得该类实例
        public static LazyBase getInstance() {
            if (instance == null) {
                instance = new LazyBase();
            }
            return instance;
        }
    }

    JDK例子(java.awt.Desktop.getDesktop()):

    public class Desktop {
        public static synchronized Desktop getDesktop(){
            if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
            if (!Desktop.isDesktopSupported()) {
                throw new UnsupportedOperationException("Desktop API is not " +
                                                        "supported on the current platform");
            }
    
            sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
            Desktop desktop = (Desktop)context.get(Desktop.class);
    
            if (desktop == null) {
                desktop = new Desktop();
                context.put(Desktop.class, desktop);
            }
    
            return desktop;
        }
    }

    5.懒汉---线程安全:这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

    如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——线程安全 
    但是给gitInstance方法加锁,避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。---效率低

    /**
     * 懒汉 多线程安全 效率低(99%情况下不需要同步)
     */
    public class LazySyn {
        //静态属性用来缓存创建实例
        private static LazySyn instance = null;
    
        //私有构造方法避免程序自由创建实例
        private LazySyn() {
        }
    
        //静态公共方法用于取得该类实例
        public static synchronized LazySyn getInstance() {
            if (instance == null) {
                instance = new LazySyn();
            }
            return instance;
        }
    }

    6.1懒汉---双重检查

    可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。

      “双重检查加锁”指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

    /**
     * 懒汉 多线程安全 双重检查
     */
    public class LazyDoubleCheck {
        //静态属性用来缓存创建实例
        private static LazyDoubleCheck instance = null;
    
        //私有构造方法避免程序自由创建实例
        private LazyDoubleCheck() {
        }
    
        public static LazyDoubleCheck getInstance() {
            if (instance == null) {
                synchronized (LazyDoubleCheck.class) {
                    if (instance == null) {
                        instance = new LazyDoubleCheck();
                    }
                }
            }
            return instance;
        }
    }

    第一个if (instance == null),是为了解决效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。 
    第二个if (instance == null),是为了防止可能出现多个实例的情况。

    6.2懒汉---终极版本(volatile)

    知识点:什么是原子操作? 

    简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。 
    比如,简单的赋值是一个原子操作:

    m = 6; // 这是个原子操作 
    假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

    而,声明并赋值就不是一个原子操作:

    int n = 6; // 这不是一个原子操作 
    对于这个语句,至少有两个操作: 
    ①声明一个变量n 
    ②给n赋值为6 
    ——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。 
    ——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

    知识点:什么是指令重排? 
    简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。 
    比如,这一段代码:

    int a ; // 语句1 
    a = 8 ; // 语句2 
    int b = 9 ; // 语句3 
    int c = a + b ; // 语句4 
    正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。 
    但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。 
    由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。 
    ——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

    OK,了解了原子操作和指令重排的概念之后,我们再继续看6.1代码的问题。 

    主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。 
    1. 给 instance 分配内存 
    2. 调用 LazyDoubleCheck 的构造函数来初始化成员变量,形成实例 
    3. 将instance对象指向分配的内存空间(执行完这步 instance才是非 null 了) 
    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。 
    再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。 
    这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作(当然这种概率已经非常小了,但毕竟还是有的嘛,这时候就要用到volatile)。

    volatile:volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量;是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。

    public class LazyVolatile {
        //volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
        private volatile static LazyVolatile instance = null;
    
        //私有构造方法避免程序自由创建实例
        private LazyVolatile() {
        }
    
        public static LazyVolatile getInstance() {
            if (instance == null) {////先检查实例是否存在,不存在,在进行同步
                synchronized (LazyVolatile.class) { //同步块,线程安全的创建实例
                    if (instance == null) {//再次检查实例是否存在,如果不存在才真正的创建实例  
                        instance = new LazyVolatile();
                    }
                }
            }
            return instance;
        }
    }

    注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null)),大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的。  

    7.枚举(jdk1.5+):不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

    /**
     * jdk1.5 enum特性
     */
    public enum EnumSingleton {
        INSTANCE;
        public void whateverMethod() {
        }
    }

    大自然的搬运工(感谢以下作者):

           hxxp://blog.csdn.net/zhshulin/article/details/38225733

           hxxp://blog.csdn.net/picway/article/details/70163455

           hxxp://blog.csdn.net/qq_27550755/article/details/49781683

  • 相关阅读:
    Google-Hack
    DnsLog盲注
    utf-8编码转换问题
    sql注入 无列名注入
    Python正则
    变形--缩放 scale()
    变形--扭曲 skew()
    变形--旋转 rotate()
    Css3中的变形与动画
    关于伪类元素:before和:after
  • 原文地址:https://www.cnblogs.com/manusas/p/6734214.html
Copyright © 2011-2022 走看看