单例模式

一、什么是单例?

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

二、为什么需要单例?

    第一、为了节省内存、节省计算。在很多时候我们只需要一个单例就够了,如果出现了更多实例,反而属于浪费。举个例子(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")这样的异常,反射创建失败。可以看出枚举方式能防止反序列化和反射破坏单例,这一点上有很大的优势,安全问题不容小事,一旦生成了多个实例单例模式就彻底没用了,

posted @ 2020-02-12 12:38  伯爵~  阅读(106)  评论(0编辑  收藏  举报