设计模式(一)之单例模式

2021-01-28

单例模式(Singleton Pattern)

单例模式:Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

特点: 

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

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

具体实现

    (一)饿汉式

    写法一(最常用的饿汉式写法):

     描述:比较常用,但容易产生垃圾对象。
     优点:JVM保证单例,不需要加锁就能保证线程安全,执行效率会提高。
     缺点:在类加载时就进行实例化对象,浪费内存。

 1 public class SingletonPatternDemo1 {
 2 
 3     private static SingletonPatternDemo1 singleton = new SingletonPatternDemo1(); 4 5 private SingletonPatternDemo1(){ 6 7  } 8 9 public static SingletonPatternDemo1 getInstance(){ 10 return singleton; 11  } 12 13 }
    写法二(通过静态代码块实现):

        写法一和写法二本质是相同的,都是饿汉式的实现方式,只是代码实现不同。

 1 public class SingletonPatternDemo2 {
 2     private static final SingletonPatternDemo2 INSTANCE; 3 4 static{ 5 INSTANCE = new SingletonPatternDemo2(); 6  } 7 8 private SingletonPatternDemo2(){ 9 10  } 11 12 public static SingletonPatternDemo2 getInstance(){ 13 return INSTANCE; 14  } 15 }

 

 (二)懒汉式

             写法一:

      描述:针对饿汉式在类加载时就进行对象实例化,浪费内存的缺点进行改进,使在加载类时只是申明唯一的内对象,并不进行对象实例化;而在第一次使用时进行对象的实例化。

      优点:达到了在需要时初始化的目的。

      缺点:多线程访问时会存在问题,不能保证线程安全

 1 public class SingletonPatternDemo3 {
 2     private static SingletonPatternDemo3 INSTANCE; 3 4 private SingletonPatternDemo3(){ 5 6  } 7 8 public static SingletonPatternDemo3 getInstance(){ 9 if (INSTANCE == null){ 10 INSTANCE = new SingletonPatternDemo3(); 11  } 12 return INSTANCE; 13  } 14 15 public static void main(String[] args){ 16 for (int i = 0 ; i < 100 ; i++){ 17 new Thread(() -> { 18  System.out.println(SingletonPatternDemo3.getInstance().hashCode()); 19  }).start(); 20  } 21  } 22 }
    /*这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
     这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。*/
   写法二:

      描述:对于写法一虽然达到了使用时初始化但是线程不安全的特点,因而催生出了写法二使用synchronized加锁来保证线程安全。

      优点:达到了在需要时初始化的目的,解决了多线程不安全的问题。

      缺点:使用了synchronized加锁的方式来解决线程安全性问题,尤其是在class中static方法上加锁,极大地降低了代码的效率。

 1 public class SingletonPatternDemo4 {
 2 
 3     private static SingletonPatternDemo4 INSTANCE; 4 5 private SingletonPatternDemo4(){ 6 7  } 8 9 public static synchronized SingletonPatternDemo4 getInstance(){ 10 if (INSTANCE == null){ 11 INSTANCE = new SingletonPatternDemo4(); 12  } 13 return INSTANCE; 14  } 15 16 public static void main(String[] args){ 17 for (int i = 0 ; i < 100 ; i++){ 18 new Thread(() -> { 19  System.out.println(SingletonPatternDemo4.getInstance().hashCode()); 20  }).start(); 21  } 22  } 23 24 }
     写法三:

      描述:在写法二中通过加锁的方式解决了多线程安全性的问题,但同时由于加锁位置导致代码效率极低;因而改变加锁的位置是否就能满足线程安全又能挽救一部分代码效率呢?大胆的做出假设是否不再静态类方法上进行加锁,改在需要保障线程安全的代码块上加锁,是不是就能既能保障线程安全也能提供代码效率,催生出第三种在实例化对象的代码块上进行加锁。

      优点:达到了在需要时初始化的目的,一定程度上提高了代码效率。

      缺点:事实证明并不能保障线程安全。

 1 public class SingletonPatternDemo5 {
 2     private static SingletonPatternDemo5 INSTANCE; 3 4 private SingletonPatternDemo5(){ 5 6  } 7 8 public static SingletonPatternDemo5 getInstance(){ 9 if (INSTANCE == null){ 10 //妄图通过减少同步代码块的方式提高效率,然而不可行 11 synchronized(SingletonPatternDemo5.class) { 12 INSTANCE = new SingletonPatternDemo5(); 13  } 14  } 15 return INSTANCE; 16  } 17 18 public static void main(String[] args){ 19 for (int i = 0 ; i < 100 ; i++){ 20 new Thread(() -> { 21  System.out.println(SingletonPatternDemo5.getInstance().hashCode()); 22  }).start(); 23  } 24  } 25 }

    通过分析代码:发现存在问题是因为当线程A和线程B并发执行,先是线程A执行到if(INSTANCE == null){}判断并满足条件,此时线程B也执行到此判断,但是线程A正在申请或刚刚申请到锁还没有执行到实例化对象,那么线程B的判断也是true,并进入等待锁的释放,因而线程A和B都会执行实例化对象,就会产生两个对象。所以这种写法不能保证线程安全。

              也行存在这样的疑或,为什么不把判断放进synchronized代码块中,这样就能保证线程安全。这种写法与写法二并没有本质的不同。

           写法四(双检锁/双重校验锁(DCL,即 double-checked locking)------以前认为最完美的方式):

      描述:通过写法三以及写法三的分析,在synchronized加锁前后都进行判断就能满足要求,因而诞生了双重判断的写法。

      优点:达到了在需要时初始化的目的,一定程度上提高了代码效率,线程安全。

      缺点:使用了synchronized加锁就有效率的损失,而且代码也越来越复杂。

 1 public class SingletonPatternDemo6 {
 2 
 3     private static volatile SingletonPatternDemo6 INSTANCE;//加上volatile是因为需要解决在进行JIT设置时存在的语句重排问题
 4 
 5     private SingletonPatternDemo6(){ 6 7  } 8 9 public static SingletonPatternDemo6 getInstance() { 10 if (INSTANCE == null) {//第一个判断有必要吗?有必要,当一些线程判断到INSTANCE!=null时就返回了,不用都去竞争这把锁,提高代码效率 11 //双重判断 12 synchronized (SingletonPatternDemo6.class){ 13 if (INSTANCE == null) { 14 INSTANCE = new SingletonPatternDemo6(); 15  } 16  } 17  } 18 return INSTANCE; 19  } 20 21 public static void main(String[] args){ 22 for (int i = 0 ; i < 100 ; i++){ 23 new Thread(() -> { 24  System.out.println(SingletonPatternDemo6.getInstance().hashCode()); 25  }).start(); 26  } 27  } 28 29 }

   (三)登记式 / 静态内部类实现

      描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

      优点:JVM保证单例,不存在多线程不安全问题,加载外部类的时候不会加载内部类,可以实现懒加载,能够在使用时才进行实例化。

 1 public class SingletonPatternDemo7 {
 2 
 3     public SingletonPatternDemo7(){ 4 5  } 6 7 private static class SingletonPatternDemo7Holder{ 8 private static final SingletonPatternDemo7 INSTANCE = new SingletonPatternDemo7(); 9  } 10 11 public static SingletonPatternDemo7 getInstance() { 12 return SingletonPatternDemo7Holder.INSTANCE; 13  } 14 15 public static void main(String[] args){ 16 for (int i = 0 ; i < 100 ; i++){ 17 new Thread(() -> { 18  System.out.println(SingletonPatternDemo7.getInstance().hashCode()); 19  }).start(); 20  } 21  } 22 }

   (四)枚举实现

      描述:这种方式是JAVA创始人之一, 《Effective Java》的作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,但在实际中基本不用,因为这需要把class类改为enum类。

      优点:不仅保证了多线程的问题,还能防止反序列化。

 1 public enum SingletonPatternDemo8 {
 2 
 3  INSTANCE; 4 5 public static void main(String[] args){ 6 for (int i = 0 ; i < 100 ; i++){ 7 new Thread(() -> { 8  System.out.println(SingletonPatternDemo8.INSTANCE.hashCode()); 9  }).start(); 10  } 11  } 12 }

写在最后:一般情况下,使用饿汉式-写法一就可以了。只有在要明确实现 lazy loading 效果时,才会使用(三)登记式 / 静态内部类实现的方式。如果涉及到反序列化创建对象时,可以尝试使用(四)枚举实现的方式。如果有其他特殊的需求,可以考虑使用 懒汉式-写法四(双检锁/双重校验锁(DCL,即 double-checked locking))的方式。