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

    一、什么是单例?

      单例模式指的是保证一个类只有一个实例,并且提供一个全局可以访问的入口。举个例子:就像分身术,虽然分身有很多,但是每一个分身都对应同一个真身。

    二、为什么需要单例?

        第一、为了节省内存、节省计算。在很多时候我们只需要一个单例就够了,如果出现了更多实例,反而属于浪费。举个例子(Example A),

    /**
    *Example A
    */
    public class ExpensiveSource{
      public ExpensiveSource(){
        field1 = //查询数据库
        field2 = //大量的计算
        field3 = //其它耗时操作
      }
    }

      我们拿一个初始化比较耗时的类来说,在实例话这个类的时候,要从数据库进行很多的查询,然后还要做很多的计算,所以在第一次构造的时候,我们需要花费很多时间来实例化这个对象。但是假设我们数据库例的数据是不变的,并且我们把这个对象保存到了内存当中,那么以后我们就可以使用同一个实例了。如果我们每次都重新生成新的实例化,那实在是没有必要。

      第二、保证结果的正确,比如我们需要一个全局的计数器,用来统计人数,如果有多个实例的话,反而造成混乱。另外,为了方便管理,很多工具类我们只需要一个实例,那么我们通过提供一个统一入口,比如getInstance()方法,就可以获取到一个单例,这是很方便的。太多的实例不但没有帮助,反而让我们眼花缭乱。

    三、单例模式的适用场景

      1、无状态的工具类,如日志工具,我们处理用它记录日志之外,并不需要在它的实例上做任何存储状态,这时候我们只需要一个实例就可以了。

      2、全局信息类,全局计数、环境变量等,比如我们在一个类上记录网站的访问次数,我们不希望有写记录在对象A上,有些记录在对象B上,这时候我们就可以使用单例对象来记录,在需要记录的时候拿出来用就可以了。对于全局的环境变量类也是如此。

    四、常见的写法

    1. 饿汉式

     1 /**
     2  * 饿汉式
     3  */
     4 public class Singleton{
     5   //用static修饰实例
     6   private static Singleton singleton = new Singleton();
     7   //构造函数用private修饰
     8   private Singleton(){
     9 
    10   }
    11 
    12   public static Singleton getInstance(){
    13     return singleton;
    14   }
    15 }

     1 /**
     2  * 静态代码库式
     3  */
     4 public class Singleton{
     5   
     6   private static Singleton singleton;
     7   
     8   static{
     9     singleton = new Singleton();
    10   }
    11 
    12   private Singleton(){
    13 
    14   }
    15 
    16   public static Singleton getInstance(){
    17     return singleton;
    18   }
    19 }

      在类装载时就完成了实例化,避免了线程同步的问题。缺点是在类装载是就完成了实例化,而没有达到懒加载的效果,如果这个从始至终实例没被使用,就会造成内存的浪费。这个和饿汉式的加载过程类似,只不过把实例化放在了静态代码块中进行。也是在类加载时,就执行了静态代码块中的代码,完成了实例的初始化,所以缺点也是如果类不被使用,就会造成内存的浪费。

      2、懒汉式

     1 /**
     2  * 懒汉式
     3  */
     4 public class Singleton{
     5   
     6   private static Singleton singleton;
     7   
     8   private Singleton(){
     9 
    10   }
    11 
    12   public static Singleton getInstance(){
    13     if(singleton == null){ //1
    14         singleton = new Singleton(); //2
    15     }
    16     return singleton;
    17   }
    18 }

      这中写法在调用getInstance()方法时才去实例化我们的对象,起到了懒加载的效果,但是只能在单线程下使用,如果在多线程下使用,一个线程进入了1位置,还没来得及执行2处代码,另外一个线程也进入了1处代码,然后实例化了对象,而第一个已经做了为空的判断,所以也会执行2处代码,这时就会出现多次创建实例。所以在多线程环境下不能使用这种方式。多线程环境下这样写是错误的。那么线程安全的懒汉式该怎么写呢?我们在getInstance()方法上加synchronized关键字,以此来解决刚才的线程安全问题。不过这种方法的缺点就是效率低下,每个线程在执行getInstance()方法获取实例时都要进行同步。多个线程不能同时访问,这在大多时候是没有必要的。那么为了提高效率,缩小同步范围,就把synchronized关键字从方法上移除了,然后再把synchronized关键字刚在方法内部,采用代码块的方式保证线程安全。

     1 /**
     2  * 懒汉式
     3  */
     4 public class Singleton{
     5   
     6   private static Singleton singleton;
     7   
     8   private Singleton(){
     9 
    10   }
    11 
    12   public static Singleton getInstance(){
    13     if(singleton == null){ //1
    14       synchronized(Singleton.class){ //2
    15         singleton = new Singleton(); //3
    16       }
    17     }
    18     return singleton;
    19   }
    20 }

      不过这种写法也是有问题的,假如一个线程执行了1处代码,但还没往下执行,这时候另外一个线程也执行了1处代码,然后获取锁了锁,执行完3处代码并释放锁后,第一线程就直接获取锁,并执行3处代码,这也会出现多次创建实例。所以为了解决这个问题,就有了另一种写法,双重检查式。

      3、双重检查式

     1 /**
     2  * 双重检查式
     3  */
     4 public class Singleton{
     5   
     6   private static volatile Singleton singleton;
     7   
     8   private Singleton(){
     9 
    10   }
    11 
    12   public static Singleton getInstance(){
    13     if(singleton == null){
    14       synchronized(Singleton.class){
    15         if(singleton == null){
    16           singleton = new Singleton();
    17         }
    18       }
    19     }
    20     return singleton;
    21   }
    22 }

      我们重点看一下getInstance()方法,我们进行了两次singleton==null判断,就可以保证线程安全了,这样实例化代码只会被调用一次,后边只需要调用第一个if就可以了,然后会跳过整个if块,直接return实例化对象,这种写法的优点时不仅线程安全,而且延迟加载,效率也会更高。那么去掉第一个if判断语句块行不行呢,答案肯定是不行,如果去掉第一个if块,所有的线程都会串行执行,效率会很低,所以两个check都要保留。

      那为什么要加volatile关键字呢,主要是因为singleton = new Singleton()这句话,这并非是一个原子操作,事实上在JVM中,这一句至少做了三件事,

        第1步:给singleton分配内存空间,

        第2步:调用Singleton的构造函数等来初始化singleton

        第3步:将singleton对象执行分配的内存空间(执行完这步singleton就不是null了)

      这里要留意一下这三步的顺序,因为存在着重排序的优化,也就是第2步和第3步的顺序是不能保证的,最终的顺序可能是1-2-3,也可能是1-3-2,如果是1-3-2的话,那么在第3步执行完之后,singleton就不是null了,假设此时线程2进入了getInstance()方法,因为这时singleton已经不是null了,所以他就会通过第一层检查,直接返回,但这时对象并没有完全被初始化,所以在使用这个对象的时候就会报错。所以使用volatile这个关键字的主要意义就是防止刚才的重排序的发生,避免了拿到未完全初始化的对象。

    4、静态内部类的写法

     1  /**
     2   * 静态内部类方式
     3   */
     4  public class Singleton{   
     5    
     6    private Singleton(){}
     7 
     8    private class static SingletonInstance{
     9       private static final Singleton singleton = new Singleton();
    10    }
    11  
    12    public static Singleton getInstance(){
    13      return SingletonInstance.singleton;
    14    }
    15  }

      它跟饿汉式采用的机制类似,都采用了类装载的机制,来保证我们初始化实例的只有一个线程,所以这里,JVM帮助我们保证了线程的安全性。不过呢,饿汉式有一个特点呢就是只要singleton这个类被加载了,就会实例化这个单例对象,而静态内部类这种方式在类装载时并不会立刻实例化这个对象,而是在需要实例时,也就是在调用getInstance()方法时才会去实例化这个对象。

    这里做个小总结,静态内部类的方式和双重检查式的有点是一样的,都是避免了线程不安全的问题,并且实现了延迟加载。效率高, 可以看出,两种方式都是不错的写法,但是他们不能防止反序列化生成多个实例,所以更好的方式就是枚举类的方法。

    5、枚举类

     1  /**
     2   * 枚举方式
     3   */
     4  public enum Singleton{   
     5    
     6    INSTANCE;
     7  
     8    public void  whateverMethod(){
     9    }
    10  }

    借助JDK1.5中添加的枚举类来实现单例模式。这不仅能避免多线程同步的问题,还能防止反序列化和反射来创建新的对象,来破坏单例的情况出现。

      Joshua Bloch说过使用枚举实现单例的方式虽然还没有被广泛采用,但是单元素的枚举类型已经成为了实现Singleton的最佳方法。为什么他比较推崇枚举的这种方式呢,那就要回到枚举这种方式的优点上来说了,枚举写法的优点有这么几个,首先是枚举类写法简单,不需要我们自己去考虑懒加载、线程安全等问题,同时代码比较短小精悍,比其他任何方式都更简洁,第二个优点是线程安全有保障,通过反编译一个枚举类我们可以发现,枚举中的各个枚举项,是通过static代码块来定义和初始化的,他们会在类加载时完成初始化,而Java类的加载由JVM来保证线程的安全,所以呢创建一个Enum类型的枚举是线程的安全的。前面集中方式是可能存在问题的,那就是存在被反序列化破坏,反序列化生成的新的对象从而产生多个实例。java是对枚举的序列化做了规定,在序列化时,仅仅是将枚举对象的name属性输出到结果中,在反序列化时,就是通过java.lang.EnumvalueOf方法来根据名字查找对象,而不是新建一个新的对象,所以这就防止了反序列化导致单例破坏问题的出现。对于反射破坏单例问题,枚举类同样有防御措施,反射在通过newInstance创建对象时,会检查这个类是否是枚举类,如果是的话就抛出illegalArguementException("cannot reflectively create Enum objects")这样的异常,反射创建失败。可以看出枚举方式能防止反序列化和反射破坏单例,这一点上有很大的优势,安全问题不容小事,一旦生成了多个实例单例模式就彻底没用了,

  • 相关阅读:
    iOS电商类App研发学习总结
    Swift4.0复习闭包
    Swift4.0复习函数
    Swift4.0复习Optional
    SQL面试题
    sql(join on 和where的执行顺序)
    算法四:回溯和分支界定
    算法三:贪婪算法
    编程之美2.11:寻找最近的点对
    编程之美2.5:寻找最大的K个数
  • 原文地址:https://www.cnblogs.com/shuaixiaobing/p/12293932.html
Copyright © 2011-2022 走看看