剑指offer第二版面试题2:实现Singleton模式(java)

面试题2:实现Singleton模式

题目:设计一个类,我们只能生成该类的一个实例。
由于第一题主要讲的是C++语言特性,因此直接跳过,开始记录第二题。

单例模式分为懒汉式(需要才去创建对象)和饿汉式(创建类的实例时就去创建对象)。

  1. 饿汉式

    该模式在类被加载时就会实例化一个对象。

    • 属性实例化对象
    //饿汉模式:线程安全,耗费资源。
    public class HugerSingletonTest {
        //final 该对象的引用不可修改 
        private static final HugerSingletonTest ourInstance = new HugerSingletonTest();
    	//static 可以通过HugerSingletonTest.getInstance()实现调用。
        //HugerSingletonTest hunger = HugerSingletonTest.getInstance();
        public static HugerSingletonTest getInstance() {
            return ourInstance;
        }
    
        private HugerSingletonTest() {}//private 防止其他类初始化该类
    }
    

    该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)

    PS:在看这块儿时发现static已经忘得差不多了,重新回顾了一下。。。static关键字的四种用法

  2. 懒汉式

    该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)

    • 非线程安全

      public class Singleton {
          private static Singleton ourInstance;
          public static Singleton getInstance() {
              if (null == ourInstance) {
                  ourInstance = new Singleton();
              }
              return ourInstance;
          }
          private Singleton() {}
      }
      

      分析:如果有两个线程同时调用getInstance()方法,则会创建两个实例化对象。所以是非线程安全的。

    • 线程安全:给方法加锁(资源消耗较大)

      public class Singleton {
          private static Singleton ourInstance;
          public synchronized static Singleton getInstance() {
              if (null == ourInstance) {
                  ourInstance = new Singleton();
              }
              return ourInstance;
          }
          private Singleton() {}
      }
      

      分析:如果有多个线程调用getInstance()方法,当一个线程获取该方法,而其它线程必须等待,消耗资源。

      这里的消耗资源主要是因为synchronized关键字加在了函数上。

    • 线程安全:双重检查锁(DCL Double-Check懒汉式)

      public class Singleton {
          //volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性
          private volatile static Singleton ourInstance;
          public static Singleton getInstance() {
              if (null == ourInstance) {
                  synchronized (Singleton.class) {
                      if (null == ourInstance) {
                          ourInstance = new Singleton();
                      }
                  }
              }
              return ourInstance;
          }
          private Singleton() {}
      }
      

      分析:为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。

      相较于上一个,因为是在函数内部加锁,所以消耗资源较少。

      volatile作用:

      • 创建一个对象,往往包含三个过程:对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含的三个过程。
      • 1.给 singleton 分配内存。
      • 2.调用 Singleton 的构造函数来初始化成员变量,形成实例。
      • 3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)。
      • 但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被l另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了。
      • 针对这种情况,我们有什么解决方法呢?那就是把singleton声明成 volatile。
      • volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性。
    • 线程安全:静态内部类(剑指强烈推荐的方法)

      public class Singleton {
          private static class SingletonHodler {
              private static final Singleton ourInstance = new Singleton();
          }
          public static Singleton getInstance() {
              return SingletonHodler.ourInstance;
          }
          private Singleton() {}
      }
      

      分析:利用静态内部类的特性:只会加载一次。某个线程在调用该方法时会创建一个实例化对象。


      下面的枚举和ThreadLocal我暂时看不懂,待到将来看到该部分时,再继续研究。

    • 线程安全:枚举

      enum SingletonTest {  
          
          INSTANCE,  
          INSTANCE("aa"),
          private String bb = "a";
          public whateverMethod() {
              
          }
          public SingletonTest(String aa) {
              aa = bb;
          }
      }
      
      

      分析:枚举的方式是《Effective Java》书中提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。

    • 线程安全:使用ThreadLocal

      public class Singleton {
          private static final ThreadLocal<Singleton> tlSingleton =
                  new ThreadLocal<Singleton>() {
                      @Override
                      protected Singleton initialValue() {
                          return new Singleton();
                      }
             // () -> return new Singleton();  lamda表达式
                  };
          public static Singleton getInstance() {
              return tlSingleton.get();
          }
          private Singleton() {}
      }
      

      分析:ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    • 线程安全:CAS锁

      public class Singleton {
          private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
          /**
           * 用CAS确保线程安全
           */
          public static Singleton getInstance() {
              while (true) {
                  Singleton current = INSTANCE.get();
                  if (current != null) {
                      return current;
                  }
                  current = new Singleton();
                  if (INSTANCE.compareAndSet(null, current)) {
                      return current;
                  }
              }
          }
          private Singleton() {}
      }
      

      分析:对AtomicReference和AtomicBoolean详解 compareAndSet详解

      INSTANCE.compareAndSet(null, current)是将null与INSTANCE中的引用比较,如果相同,返回true,并将current赋给INSTANCE,如果不同,则返回false并且不赋值。此方法使用设置的内存语义更新值,就像将该变量声明为volatile一样,不改变执行语序。

      当线程1与线程2同时到达current = new Singleton()时,假设线程1先进入if,此时INSTANCE为null,与形参null相同,因此将current赋值给INSTANCE,并返回current。然后线程2进入if,此时INSTANCE与null相比较为false,跳出循环,重新进入,再次get()后赋值为Singleton对象,在第一个if判断中跳出。

参考链接:【设计模式】单例设计模式 深入浅出单实例Singleton设计模式

posted @ 2021-11-26 21:45  杀戒之声  阅读(48)  评论(0编辑  收藏  举报