zoukankan      html  css  js  c++  java
  • 创建对象——单例(Singleton)模式

     

    单例(Singleton)模式

     
    保证一个类在系统里只能有一个对象被实例化。
     
    如:缓存池、数据库连接池、线程池、一些应用服务实例等。
     
    难点:在多线程环境中,保证实例的唯一性。
     
     

    最简单的单例模式:

    1. 保证该类构造方法是私有的,外部无法创建该类型的对象;
    2. 提供一个全局访问点,方便给客户对象提供对此单例对象的使用;
     
    public class Singleton {
        /**
         * 私有变量,外界无法访问
         * 可以定义 public 类型 instance变量,把属性直接暴露给客户对象,则没必要实现getInstance()方法
         * 但是可读性降低,而且直接暴露实例变量的名字给客户程序,会增加代码的耦合度
         */
        private static Singleton instance = new Singleton();
     
        static {
            //...
        }
     
     
        // 唯一的 private构造方法,客户对象无法创建该对象实例
        private Singleton() {
     
        }
     
     
        // 全局访问点
        public static Singleton getInstance() {
            return instance;
        }
    }
     
    // 客户使用单例模式代码
    Singleton singleton = Singleton.getInstance();
        如果该实例需要比较复杂的初始化过程时,把这个过程应该写在 static{ ... }代码快中。
        注意:此实现是线程安全的,当对个线程同时去访问该类的 getInstance( ) 方法时,不会初始化多个不同的对象,这是因为,JVM 在加载此类时,对于 static 属性的初始化只能由一个线程执行且仅一次。
     
     

    进阶:

        Statci 在加载类时就会被初始化,出于性能等方面的考虑,我们希望延迟实例化单例对象,只有在第一次使用该类的实例时才去实例化。
     
    延迟创建
     
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
        我们把单例的实例化过程移至 getInstance( )方法,而不是在加载类时预先创建,当访问此方法时,首先判断该实例是不是已经被实例化过了,如果已被初始化,则直接返回这个对象的引用;否则,创建这个实例并初始化,最后返回这个对象引用。
     
        使用 if (instance == null) 判断是否实例化完成了,此方法不是线程安全的。
     
     
    线程安全
        在高并发的环境中,getInstance( ) 方法将返回多个指向不同的该类实例。
     
      Thread 1 Thread 2
    1 if (instance == null)  
    2   if (instance == null)
    3 Singleton instance = new Singleton();  
    4   Singleton instance = new Singleton();
    5 return instance;  
    6   return instance;
     
        在时刻 1和 2,由于还没有创建单例对象,Thread 1 和 Thread 2都会进入创建单例实例的代码块分别创建实例。在时刻 3 ,Thread 1创建了一个实例对象,但是 Thread 2此时已无法知道,于是继续创建一个新的实例对象,导致这两个线程持有的实例并非为同一个。
        更为糟糕的是,在没有自动内存回收机制的语言平台上运行这样的单例模式,如:C++,以为我们认为创建了一个单例实例,忽略了其他线程所产生的对象,不会手动去回收它们,从而引起内存泄漏。
     
    为了解决这个问题,我们给次方法添加 关键字,代码如下:
     
      
    public   static   synchronized  Singleton getInstance() {
         if  ( instance  ==  null ) {
             instance  =  new  Singleton();
         }
         return   instance ; 
    }
     
        这样,再多的线程访问都只会实例化一个单例对象,实现了多线程的安全访问,但是在多线程高并发访问的情况下,给此方法加上 ynchronized 关键字会是得性能大不如前。
     
     
    如何创建并发访问效率高的单例Double-Check Locking
     
        仔细分析发现,使用 synchronized 关键字对整个 getInstance( ) 方法进行同步是没有必要的:我们只要保证实例化这个对象的那段逻辑被一个线程执行就可以了,而返回引用的那段代码是没有必要同步的。更改去下:
    public   static  Singleton getInstance() {
         if  ( instance  ==  null ) {
             synchronized  (Singleton. class ) {
                 if  ( instance  ==  null ) {
                     instance  =  new  Singleton();
                }
            }
        }
        return   instance ;
    }
     
        在 getInstance( )方法里,首先判断实例是否已经被创建了,如果还没有创建,首先使用 synchronized 同步实例代码块。在同步代码块里,还需要再次检查是否已经创建了此类的实例,这是因为:如果没有第二次检查,这时有两个线程 Thread A 和 Thread B 同时进入该方法,它们都检测到 instatnce 为 null,不管哪一个线程先占据同步锁,并创建实例对象,都不会阻止另外一个线程继续进入实例代码块重新创建实例对象,这样,同样会产生两个实例对象。所以,我们在同步的代码块里,要进行第二次判断,判断该代码是否已被创建。
     
     
    注意:此程序只有在 JAVA 5及以上版本才能正常运行,在以前版本不能保证其正常运行。这是由于 Java平台的内存模式容许 out-of-order writes 引起的,假定有两个线程,Thread 1 和 Thread 2,它们执行以下步骤:
        1、Thread 1发现 instatnce 没有被实例化,它获得锁,并去实例化此对象,JVM 容许在没有完全实例化完成时,instance 变量就指向此实例,因为这些步骤可以是 out-of-order writes 的,此时 instance==null 为 false,之前的版本即使用 volatile 关键字修饰也无效。
        2、在初始化完成之前,Thread 2 进入此方法,发现 instance 已经不为 null了,Thread 2 便认为该实例初始化完成了,使用这个未完成初始化的实例对象,则很可能引起系统的奔溃。
     
     
     
    Initialization on demand holder
        要使用线程安全的延迟的单例初始化,还有一种方法,代码如下:
    public   class   LazyLoadedSingleton  {
         private  LazyLoadedSingleton {
            
        }
         private   static   class  LazyHolder{
             private   static   final  LazyLoadedSingleton  singletonInstatnce =  new  LazyLoadedSingleton();
        }
         public   static  LazyLoadedSingleton getInstance() {
             return  LazyHolder.singletonInstatnce;
        }
    }
     
        当 JVM 加载 LazyLoadedSingleton   类时,由于该类没有 static 属性,所以加载完成后便即可返回。只有第一次调用 getInstance( ) 方法时,JVM 才会加载 LazyHolder 类,由于它包含一个 static 属性 singletonInstatnce,所以会首先初始化这个变量,这样即实现了一个即保证线程安全又支持延迟加载的单例模式。
     
    单例模式序列化应该注意的问题Singleton 的序列化
     
        如果单例类实现了 Serializable接口,在默认情况下,每次反序列化总会创建一个新的实例对象,这样一个系统会出现多个对象使用。
        解决思路: readResolve( )方法在反序列化完成之前被执行,我们在此方法里替换掉反序列化出来的那个新的实例,让其指向内存中的那个单例对象即可,代码如下:
    public   class  SerializableSingleton  implements  Serializable {
         private   static   final   long   serialVersionUID  = 4285441628073602932L;
         static  SerializableSingleton  singleton  =  new  SerializableSingleton();
         private  SerializableSingleton() {
        }
         private  Object  readResolve () {
             return   singleton ;
        }
    }
        方法 readResovle( ) 直接返回 singleton单例,这样,在内存中始终保持了一个唯一的单例对象。
     
     
     
    思考:以上学习,讨论的是在同一个 JVM中,保证一个类只有一个单例,如果在分布式环境中,如何保证在整个应用(可能分布在不同 JVM上)只有一个实例???
     
  • 相关阅读:
    P3444 [POI2006]ORK-Ploughing
    p4555&bzoj2565 最长双回文串
    bzoj2563 阿狸和桃子的游戏
    p4503&bzoj3555 企鹅QQ
    p3627&bzoj1179 抢掠计划(ATM)
    p2279&bzoj1217 消防局的设立
    p2661 信息传递(Tarjan模板)
    p2071 座位安排
    2B The least round way
    JSK 糟糕的bug
  • 原文地址:https://www.cnblogs.com/lindu/p/3185628.html
Copyright © 2011-2022 走看看