zoukankan      html  css  js  c++  java
  • 【JAVA设计模式】单例模式

    在阎宏博士的《JAVA与模式》一书中开头是这样描述单例模式的:

      作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。


    单例模式的结构

      单例模式的特点:

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

      一 饿汉式单例类

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

      上面的例子中,在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。

      饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。

    private static EagerSingleton instance = new EagerSingleton();

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

      二 懒汉式单例类(延迟初始化对象)

    懒汉式:延迟初始化对象

    非线程安全的延迟初始化对象

    public class LasySingleton {
        private static LasySingleton instance;
      
      
      private LasySingleton(){}

    public static LasySingleton getInstance() { if (instance == null) //1:A线程执行 instance = new LasySingleton(); //2:B线程执行 return instance; } }

    在LasySingleton中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。

    对于LasySingleton,我们可以对getInstance()做同步处理来实现线程安全的延迟初始化。示例代码如下:

    迟初始化。示例代码如下:

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

      上面的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。
      懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不着急。会一直等到马上要使用对象实例的时候才会创建,懒人嘛,总是推脱不开的时候才会真正去执行工作,因此在装载对象的时候不创建对象实例。

    private static LazySingleton instance = null;

      懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。

      由于对getInstance()做了同步处理,synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果getInstance()不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

     

     基于volatile的双重检查锁定的解决方案

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

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

      “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

      注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

    没有volatile:

    public class Singleton {
        private  static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance(){
            //如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。
            if(instance == null){                            //1
                synchronized (Singleton.class) {              //2加锁
                    if(instance == null){                           //3 再次检查实例是否存在,如果不存在才真正创建实例
                        instance = new Singleton();                //4 问题根源
                    }
                }
            }
            return instance;
        }
    }

    如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:

    • 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
    • 在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。

    双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。

    问题的根源

    前面的双重检查锁定示例代码的第4行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:

    memory = allocate();   //1:分配对象的内存空间
    ctorInstance(memory);  //2:初始化对象 :在对象创建后,立即调用类的构造函数,对刚生成的对象进行初始化。
    instance = memory;     //3:设置instance指向刚分配的内存地址

    上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下:

    memory = allocate();   //1:分配对象的内存空间
    instance = memory;     //3:设置instance指向刚分配的内存地址
                           //注意,此时对象还没有被初始化!
    ctorInstance(memory);  //2:初始化对象

    根据《The Java Language Specification, Java SE 7 Edition》(后文简称为java语言规范),所有线程在执行java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话来说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。上面三行伪代码的2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。

    由于单线程内要遵守intra-thread semantics,从而能保证A线程的程序执行结果不会被改变。

    回到本文的主题,Singleton示例代码的第4行(instance = new Singleton();)如果发生重排序,另一个并发执行的线程B就有可能在第1行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!下面是这个场景的具体执行时序:

    时间 线程A 线程B
    t1 A1:分配对象的内存空间  
    t2 A3:设置instance指向内存空间  
    t3   B1:判断instance是否为空
    t4   B2:由于instance不为null,线程B将访问instance引用的对象
    t5 A2:初始化对象  
    t6 A4:访问instance引用的对象  

    这里A2和A3虽然重排序了,但java内存模型的intra-thread semantics将确保A2一定会排在A4前面执行。因此线程A的intra-thread semantics没有改变。但A2和A3的重排序,将导致线程B在1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

    在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化:

    1. 不允许2和3重排序;
    2. 允许2和3重排序,但不允许其他线程“看到”这个重排序。

    添加volatile(禁止2、3重排序):

     private  static volatile Singleton instance = null;

      这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

      提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化(禁止指令重排序),所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

      根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

      

    三 基于类初始化的解决方案

    JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

    基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为Initialization On Demand Holder idiom):

      

    public class Singleton {
        
        private Singleton(){}
        /**
         *    类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
         *    没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
         */
        private static class instanceHolder{
            /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private static Singleton instance = new Singleton();
        }
        
        public static Singleton getInstance(){
            return instanceHolder.instance;
        }
    }

      当getInstance方法第一次被调用的时候,它第一次读取instanceHolder.instance,导致instanceHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

      这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

      

      初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化:

    • T是一个类,而且一个T类型的实例被创建;
    • T是一个类,且T中声明的一个静态方法被调用;
    • T中声明的一个静态字段被赋值;
    • T中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
    • T是一个顶级类(top level class,见java语言规范的§7.6),而且一个断言语句嵌套在T内部被执行。

      在示例代码中,首次执行getInstance()的线程将导致InstanceHolder类被初始化(符合情况4)。

      由于java语言是多线程的,多个线程可能在同一时间尝试去初始化同一个类或接口(比如这里多个线程可能在同一时刻调用getInstance()来初始化InstanceHolder类)。因此在java中初始化一个类或者接口时,需要做细致的同步处理。

      Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了,由虚拟机来保证它的线程安全性。

     

      

     http://www.cnblogs.com/java-my-life/archive/2012/03/31/2425631.html

    http://ifeve.com/double-checked-locking-with-delay-initialization/

  • 相关阅读:
    20172327 2018-2019-1 《程序设计与数据结构》实验三:查找与排序
    团队作业第二周
    需求规格说明书
    广度优先遍历
    团队作业第一周
    20172327 2018-2019-1 《程序设计与数据结构》第九周学习总结
    20172327 2018-2019-1 《程序设计与数据结构》实验二:树实验报告
    20172327 2018-2019-1 《程序设计与数据结构》第八周学习总结
    20172327 2018-2019-1 《程序设计与数据结构》第七周学习总结
    20172327 2018-2019-1 《程序设计与数据结构》第六周学习总结
  • 原文地址:https://www.cnblogs.com/chengdabelief/p/7481991.html
Copyright © 2011-2022 走看看