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")这样的异常,反射创建失败。可以看出枚举方式能防止反序列化和反射破坏单例,这一点上有很大的优势,安全问题不容小事,一旦生成了多个实例单例模式就彻底没用了,

  • 相关阅读:
    codeforces 616B Dinner with Emma
    codeforces 616A Comparing Two Long Integers
    codeforces 615C Running Track
    codeforces 612C Replace To Make Regular Bracket Sequence
    codeforces 612B HDD is Outdated Technology
    重写父类中的成员属性
    子类继承父类
    访问修饰符
    方法的参数
    实例化类
  • 原文地址:https://www.cnblogs.com/shuaixiaobing/p/12293932.html
Copyright © 2011-2022 走看看