DesignPattern系列__10單例模式

  • 2019 年 10 月 3 日
  • 筆記

單例模式介紹

單例模式,是為了確保在整個軟體體統中,某個類對象只有一個實例,並且該類通常會提供一個對外獲取該實例的public方法(靜態方法)。
比如日誌、資料庫連接池等對象,通常需要且只需要一個實例對象,這就會使用單例模式。

單例模式的7種模式

  1. 餓漢式
    • 靜態常量
    • 靜態程式碼塊
  2. 懶漢式
    • 執行緒不安全
    • 同步方法
    • 同步程式碼塊
  3. 雙重檢查
  4. 靜態內部類
  5. 枚舉
  6. 容器實現單例模式
  7. 執行緒池實現單例模式

下面依次來說明一下:

餓漢式(靜態常量)

通常,我們創建一個對象的方式就是new,但是,當我們考慮只創建一個實例的時候,就應該禁止外部來通過new的方式進行創建。同時,由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象
3.對外提供一個獲取實例靜態的public方法

程式碼實現:

public class Singleton1 {      public static void main(String[] args) {          HungrySingleton hungrySingleton = HungrySingleton.getInstance();          HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();          System.out.println(hungrySingleton == hungrySingleton1);      }  }    class HungrySingleton {      //1.私有化構造器      private HungrySingleton() {      }         // 2.類內部創建對象,因為步驟3是static的,      // 所以實例對象是static的      private final static HungrySingleton instance = new HungrySingleton();        //3.對外提供一個獲取對象的方法,      // 因為調用方式的目的就是為了獲取對象,      // 所以該方法應該是static的。      public static HungrySingleton getInstance() {          return instance;      }  }

運行程式顯示,我們的確只創建了一個對象實例。

小結

優點:程式碼實現比較簡單,在類載入的時候就完成了實例化,同時,該方式能夠避免執行緒安全問題。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成記憶體的浪費。
這種方式基於classloder機制避免了多執行緒的同步問題,不過, instance在類裝載時就實例化,在單例模式中大多數都是調用getInstance方法, 但是導致類裝載的原因有很多種, 因此不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance就沒有達到lazy loading的效果。
總結:這種單例模式可以使用,但是可能造成記憶體的浪費。

餓漢式(靜態程式碼塊)

該方式和第一種區別不大,只是將創建實例放在了靜態程式碼塊中。
由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象(通過靜態程式碼塊)
3.對外提供一個獲取實例靜態的public方法

程式碼實現:

public class Singleton2 {      public static void main(String[] args) {          HungrySingleton hungrySingleton = HungrySingleton.getInstance();          HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();          System.out.println(hungrySingleton == hungrySingleton1);      }  }    class HungrySingleton {      //1.私有化構造器      private HungrySingleton() {      }        // 2.類內部創建對象,因為步驟3是static的,      // 所以實例對象是static的      private final static HungrySingleton instance;        static {          instance = new HungrySingleton();      }        //3.對外提供一個獲取對象的方法,      // 因為調用方式的目的就是為了獲取對象,      // 所以該方法應該是static的。      public static HungrySingleton getInstance() {          return instance;      }  }

小結

該方式只是將對象的創建放在靜態程式碼塊中,其優點和缺點與第一種方式完全一樣。
總結:這種單例模式可以使用,但是可能造成記憶體的浪費。(同第一種)

懶漢式(執行緒不安全)

該方式的主要思想就是為了改善餓漢式的缺點,通過懶載入(在使用的時候再去載入),達到節約記憶體的目的。
由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象,懶載入,在使用的時候才去載入
3.對外提供一個獲取實例靜態的public方法

程式碼實現:

public class Singleton3 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    class LazySingleton {      //1.私有化構造器      private LazySingleton() {}        //2.類的內部聲明對象      private volatile static LazySingleton instance;        //3.對外提供獲取對象的方法      public static LazySingleton getInstance() {          //判斷類是否被初始化          if (instance == null) {              //第一次使用的時候,創建對象              instance = new LazySingleton();          }          return instance;      }  }    class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          LazySingleton instance = LazySingleton.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

執行程式後,發現了問題:

//運行結果:  執行緒Thread-0開始執行  執行緒Thread-1開始執行  執行緒Thread-1初始化對象1391273746  執行緒Thread-0初始化對象547686109

小結

優點:起到了懶載入的作用,但是只能在單執行緒情況下使用。
缺點:多執行緒下不安全,如果一個執行緒進入到if語句中阻滯(還未開始創建對象),另一執行緒進入並通過了if判斷,則會創建多個實例,這一點就違背了單例的目的。
結論:實際情況下,不要使用這種方式。

懶漢式(執行緒安全,同步方法)

思路

同上一中方式一樣,但是為了解決多執行緒安全問題,使用同步方法。

程式碼演示:

public class Singleton4 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    class LazySingleton {      //1.私有化構造器      private LazySingleton() {}        //2.類的內部聲明對象      private volatile static LazySingleton instance;        //3.對外提供獲取對象的方法      public synchronized static LazySingleton getInstance() {          //判斷類是否被初始化          if (instance == null) {              //第一次使用的時候,創建對象              instance = new LazySingleton();          }          return instance;      }  }    class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          LazySingleton instance = LazySingleton.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

運行結果如下所示:

執行緒Thread-1開始執行  執行緒Thread-0開始執行  執行緒Thread-0初始化對象681022576  執行緒Thread-1初始化對象681022576

小結

優點:起到了懶載入的效果,同時,解決了執行緒安全問題。
缺點:效率低下,每次想要獲取對象的時候,去執行getInstance()都是通過同步方法。而且,初始化對象後,再次使用的時候,應該直接return這個對象。
總結:可以在多執行緒條件下使用,但是效率低下,不推薦。

懶漢式(執行緒安全,同步程式碼塊)

思路

同樣是為了解決多執行緒安全問題,不過採用的是同步程式碼塊。首先,最先想到的是:

1.將getInstance()方法體全部加上同步鎖。

程式碼實現:

public class Singleton5 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    //對getInstance()的方法體整體加同步程式碼塊  class LazySingleton {      //1.私有化構造器      private LazySingleton() {}        //2.類的內部聲明對象      private volatile static LazySingleton instance;        //3.對外提供獲取對象的方法      public static LazySingleton getInstance() {          //同步程式碼塊          synchronized (LazySingleton.class) {              //判斷類是否被初始化              if (instance == null) {                  //第一次使用的時候,創建對象                  instance = new LazySingleton();              }          }          return instance;      }  }    class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          LazySingleton instance = LazySingleton.getInstance();  //     LazySingleton1 instance = LazySingleton1.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

運行的結果:

執行緒Thread-0開始執行  執行緒Thread-1開始執行  執行緒Thread-1初始化對象1419349448  執行緒Thread-0初始化對象1419349448

這種方式的優缺點和同步方法一樣,能夠實現多執行緒安全,但是效率低下。那麼,能不能提高一下效率呢?我們發現,每次調用getInstance()的時候,都要進入同步程式碼塊,但是,一旦對象初始化後,第二次使用的時候,應該能夠直接獲取這個對象才對。
按照這個思路,對程式碼進行更改(為了說明這個,新建一個類LazySingleton1):

2.只在初始化對象部分加上同步鎖

程式碼實現:

//為了提高效率,通過if判斷,初始化之前進入同步鎖  class LazySingleton1 {      //1.私有化構造器      private LazySingleton1() {}        //2.類的內部聲明對象      private volatile static LazySingleton1 instance;        //3.對外提供獲取對象的方法      public static LazySingleton1 getInstance() {          //判斷類是否被初始化          if (instance == null) {              //第一次使用的時候,創建對象              synchronized (LazySingleton1.class) {                  instance = new LazySingleton1();              }          }          return instance;      }

將類TestClass的run()方法進行更改,獲取的實例改為LazySingleton1類型。程式碼看上去沒有問題,那麼運行效果如何呢:

//運行結果:  執行緒Thread-1開始執行  執行緒Thread-0開始執行  執行緒Thread-1初始化對象1368942806  執行緒Thread-0初始化對象1187311731

那麼,我們發現,打臉了,多執行緒情況下,創建了兩個對象,並未達到單例的目的。

小結

  • 對整個方法體加同步程式碼塊
    可以達到要求,優缺點同同步方法。
  • 只在初始化對象的程式碼添加同步鎖
    不能滿足執行緒安全要求,實際工作中,不能使用這種方式

懶漢式(執行緒安全,雙重檢查機制)

思路

針對懶漢式的多執行緒問題,我們可謂是操碎了心:同步方法可以解決問題,但是效率太低了;同步程式碼塊則根本不能保證多執行緒安全。如何能做到「魚和熊掌兼得」呢?既然同步程式碼塊的效率較好,那麼我們就針對這個方式進行改良:雙重檢查機制,即在getInstance()內進行兩次檢查,第一次通過if判斷後,初始化對象之前,進行同步並再次進行判斷。這樣做的目的:既能解決執行緒安全問題,同時避免第二次使用對象的時候還要執行同步的程式碼。

程式碼實現:

public class Singleton6 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    class LazyDoubleCheckSingleton {      //1.私有化構造器      private LazyDoubleCheckSingleton() {}        //2.類的內部聲明對象      private volatile static LazyDoubleCheckSingleton instance;        //3.對外提供獲取對象的方法      public static LazyDoubleCheckSingleton getInstance() {          //判斷類是否被初始化          if (instance == null) {              //第一次使用,通過if判斷              //加鎖              synchronized (LazyDoubleCheckSingleton.class) {                  //拿到鎖後,初始化對象之前,再次進行判斷                  if (instance == null) {                      instance = new LazyDoubleCheckSingleton();                  }              }          }          return instance;      }  }    class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

運行結果如下所示:

//運行結果:  執行緒Thread-0開始執行  執行緒Thread-1開始執行  執行緒Thread-1初始化對象996963733  執行緒Thread-0初始化對象996963733

小結

優點:

  • 解決了上一種方式中的執行緒安全問題,同時實現了延遲載入的效果,節約記憶體;
  • 第二次使用的時候,if判斷為false,直接返回創建好的對象,避免進入同步程式碼,提高了效率;
    結論:推薦使用這種方式,實際工作中也比較常見這種方式。

靜態內部類

思路

為了實現多執行緒情況下安全,除了手工加鎖,還有別的方式。現在,我們採用靜態內部類的方式。這種方式利用了JVM載入類的機制來保證只初始化一個對象。
思路同樣是私有化構造器,對外提供靜態的公開方法;不同之處是,類的創建交給靜態內部類來時實現。

程式碼實現

public class Singleton7 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    class StaticInnerSingleton {      // 1.構造器私有化      private StaticInnerSingleton() {}        // 2.通過靜態內部類來初始化對象      private static class InnerClass {          private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();      }        // 3.對外提供獲取對象的方法      public static StaticInnerSingleton getInstance() {          return InnerClass.INSTANCE;      }  }      class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          StaticInnerSingleton instance = StaticInnerSingleton.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

運行結果:

執行緒Thread-0開始執行  執行緒Thread-1開始執行  執行緒Thread-0初始化對象1326533480  執行緒Thread-1初始化對象1326533480

OK,我們發現,這種方式達到了預期的效果。

小結

優點:

  • 這種靜態內部類的方式,通過類載入機制來保證了初始化實例時只有一個實例。
  • 類的靜態屬性只有在第一次載入類的時候初始化,而JVM能保證執行緒安全,在類的初始化過程中,只有一個執行緒能進入並完成初始化。
  • 靜態內部類方式實現了懶載入的效果,這種方式不會在類StaticInnerSingleton載入的時候進行初始化,而是在第一次使用時調用getInstance()方法初始化,能夠起到節約內次的目的。
  • 該方式的getInstance()方法,通過調用靜態內部類的靜態屬性返回實例對象,避免了每次調用時進行同步,效率高。
    結論:執行緒安全,效率高,程式碼實現簡單,推薦使用。

枚舉

思路

在靜態內部類的方式中,我們借用了JVM的類載入機制來實現了功能,同樣,還可以借用Java的枚舉來實現單例模式。

public class Singleton8 {      public static void main(String[] args) {          TestThread testThread = new TestThread();          Thread thread = new Thread(testThread);          Thread thread1 = new Thread(testThread);          thread.start();          thread1.start();      }  }    enum EnumSingleton {      INSTANCE;        public void sayHi() {          System.out.println("Hi, " + INSTANCE);      }  }    class TestThread implements Runnable {        @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          EnumSingleton instance = EnumSingleton.INSTANCE;          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());      }  }

運行結果如下:

執行緒Thread-0開始執行  執行緒Thread-1開始執行  執行緒Thread-1初始化對象1134798663  執行緒Thread-0初始化對象1134798663

小結

優點:

  • 這中方式需要在JDK1.5以上的版本中使用,利用枚舉來實現單例模式。能避免多執行緒同步問題。
  • 能防止反序列化重新創建新的對象。
  • 能防止反射機制來破斷單例模式。
    在《Effective Java》中提到了這種方式,其作者推薦。
    結論:推薦使用。

使用容器來創建單例

思路

我們可以先初始化單例對象,通過容器來管理,然後在使用的時候從容器中獲取對象。

程式碼實現:

class ContainSingleton {      private ContainSingleton() {}        private static Map<String, Object> singletonMap = new HashMap<>();        public static Object getInstance(String key) {          return singletonMap.get(key);      }        public void putInstance(String key, Object instance) {          if (StringUtils.isNotEmpty(key) && instance != null) {              if (!singletonMap.containsKey(key)) {                  singletonMap.put(key,instance);              }          }      }  }

小結

這種單例模式是有一定的安全隱患的,如果你多個執行緒去創建實例,並且key相同,是有可能創建多個實例的。這種形式,建議在使用的時候,先去使用一個執行緒初始化數據後再使用。

執行緒池實現單例模式

思路

思路也前面的幾種形式一樣,無非就是用執行緒池來創建對象而已。

程式碼實現

class ThreadLocalSingleton {      //私有化構造器      private ThreadLocalSingleton() {}        //類的內部創建單例對象      private static final ThreadLocal<ThreadLocalSingleton> instanceThreadLocal =              new ThreadLocal<ThreadLocalSingleton>() {                  @Override                  protected ThreadLocalSingleton initialValue() {                      return new ThreadLocalSingleton();                  }              };      // 獲取對象的方法      public static ThreadLocalSingleton getInstance() {          return instanceThreadLocal.get();      }  }

但是,這種形式的單例模式是要帶引號的。為什麼這麼說呢?寫一個程式碼測試一下吧:

class TestClass implements Runnable {      @Override      public void run() {          System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");          try {              //為了演示多執行緒情況              Thread.sleep(100);          } catch (InterruptedException e) {              e.printStackTrace();          }          ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();          System.out.println("執行緒" + Thread.currentThread().getName() + "初始化對象" + instance);      }  }    public class Singleton10 {      public static void main(String[] args) {          TestClass testClass = new TestClass();          Thread t1 = new Thread(testClass);          Thread t2 = new Thread(testClass);          t1.start();          t2.start();            System.out.println(ThreadLocalSingleton.getInstance());          System.out.println(ThreadLocalSingleton.getInstance());          System.out.println(ThreadLocalSingleton.getInstance());          System.out.println(ThreadLocalSingleton.getInstance());      }  }

OK , 我們發現了,多執行緒下創建了不同的對象,但是,對於同一執行緒,你多次獲取的對象始終是同一個。

小結

這種形式的單例模式,和之前的懶漢式加鎖的形式不一樣,加同步鎖的思路是犧牲時間(效率)來實現;這種形式是保證同一執行緒中的單例,
屬於犧牲空間來實現。

單例模式的序列化漏洞

在上面的枚舉類的總結中,我們提高枚舉方式能夠避免反序列化對象的時候重新建立新的對象(反序列化漏洞),那麼什麼是反序列化漏洞呢?Java對象進行反序列化的時候會通過反射機制來創建實例,反射機制的存在使得我們可以越過Java本身的靜態檢查和類型約束,在運行期直接訪問和修改目標對象的屬性和狀態。這裡理解的不是很準確,有錯誤的話請指出。

程式碼演示:

public class Test {      public static void main(String[] args) throws IOException, ClassNotFoundException {  //        HungrySingleton instance = HungrySingleton.getInstance();  //        //序列化  //        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));  //        oos.writeObject(instance);  //  //        //反序列化  //        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));  //        HungrySingleton newInstance = (HungrySingleton) ois.readObject();            LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();          //序列化          ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));          oos.writeObject(instance);            //反序列化          ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));          LazyDoubleCheckSingleton newInstance = (LazyDoubleCheckSingleton) ois.readObject();            System.out.println(instance);          System.out.println(newInstance);          System.out.println(instance == newInstance);      }    }  class HungrySingleton implements Serializable {          private static final long serialVersionUID = -4913346286867374832L;        //1.私有化構造器      private HungrySingleton() {      }        // 2.類內部創建對象,因為步驟3是static的,      // 所以實例對象是static的      private final static HungrySingleton instance;        static {          instance = new HungrySingleton();      }        //3.對外提供一個獲取對象的方法,      // 因為調用方式的目的就是為了獲取對象,      // 所以該方法應該是static的。      public static HungrySingleton getInstance() {          return instance;      }        //解決單例模式的反序列化漏洞    //    public Object readResolve() {  //        return instance;  //    }  }    class LazyDoubleCheckSingleton implements Serializable {      private static final long serialVersionUID = -8459475238793042042L;        //1.私有化構造器      private LazyDoubleCheckSingleton() {}        //2.類的內部聲明對象      private volatile static LazyDoubleCheckSingleton instance;        //3.對外提供獲取對象的方法      public static LazyDoubleCheckSingleton getInstance() {          //判斷類是否被初始化          if (instance == null) {              //第一次使用,通過if判斷              //加鎖              synchronized (LazyDoubleCheckSingleton.class) {                  //拿到鎖後,初始化對象之前,再次進行判斷                  if (instance == null) {                      instance = new LazyDoubleCheckSingleton();                  }              }          }          return instance;      }    //    public Object readResolve() {  //        return instance;  //    }  }

這裡,我們分別提供了懶漢式和餓漢式(雙重檢查)來驗證這個現象。運行後會報錯,實現Serializable介面後能夠正常運行,結果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a  com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@6d03e736  false

創建了兩個對象,沒有實現多執行緒安全。首先說明一下解決方案,然後再講解一下原理。我們發現餓漢式還是懶漢式都新增了一個方法readResolve(),將注釋取消後,再次運行的結果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a  com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a  true

奇蹟出現了,只是增加一個方法,情況完全不同了。那麼背後的原理是什麼呢?我們通過debug來講解:

1.在23行打一個斷點,進入並進入該方法:

2.我們發現,該方法首先是進行一些判斷,然後執行readObject0()方法,進入該方法查看:

//該方法完成程式碼   private Object readObject0(boolean unshared) throws IOException {          boolean oldMode = bin.getBlockDataMode();          if (oldMode) {              int remain = bin.currentBlockRemaining();              if (remain > 0) {                  throw new OptionalDataException(remain);              } else if (defaultDataEnd) {                  /*                   * Fix for 4360508: stream is currently at the end of a field                   * value block written via default serialization; since there                   * is no terminating TC_ENDBLOCKDATA tag, simulate                   * end-of-custom-data behavior explicitly.                   */                  throw new OptionalDataException(true);              }              bin.setBlockDataMode(false);          }            byte tc;          while ((tc = bin.peekByte()) == TC_RESET) {              bin.readByte();              handleReset();          }            depth++;          totalObjectRefs++;          try {              switch (tc) {                  case TC_NULL:                      return readNull();                    case TC_REFERENCE:                      return readHandle(unshared);                    case TC_CLASS:                      return readClass(unshared);                    case TC_CLASSDESC:                  case TC_PROXYCLASSDESC:                      return readClassDesc(unshared);                    case TC_STRING:                  case TC_LONGSTRING:                      return checkResolve(readString(unshared));                    case TC_ARRAY:                      return checkResolve(readArray(unshared));                    case TC_ENUM:                      return checkResolve(readEnum(unshared));                    case TC_OBJECT:                      return checkResolve(readOrdinaryObject(unshared));                    case TC_EXCEPTION:                      IOException ex = readFatalException();                      throw new WriteAbortedException("writing aborted", ex);                    case TC_BLOCKDATA:                  case TC_BLOCKDATALONG:                      if (oldMode) {                          bin.setBlockDataMode(true);                          bin.peek();             // force header read                          throw new OptionalDataException(                              bin.currentBlockRemaining());                      } else {                          throw new StreamCorruptedException(                              "unexpected block data");                      }                    case TC_ENDBLOCKDATA:                      if (oldMode) {                          throw new OptionalDataException(true);                      } else {                          throw new StreamCorruptedException(                              "unexpected end of block data");                      }                    default:                      throw new StreamCorruptedException(                          String.format("invalid type code: %02X", tc));              }          } finally {              depth--;              bin.setBlockDataMode(oldMode);          }      }

我們發現,該方法還是對傳入的對象進行一些判斷,在這裡,我們匹配到TC_OBJECT,執行對應的方法。
3.進入該方法:

4.進一步查看:

我們看到一個名為resolveEx的屬性,說明很接近了。
5.繼續往下調試:


我們發現,這三個條件都滿足,因為我們在LazyDoubleCheckSingleton類中定義了readResolve()方法。

6.if判斷通過,進入到下一個方法:

7.在該方法中,我們發現經過一些條件判斷後,通過反射方式來調用我們在類LazyDoubleCheckSingleton中新定義的方法readResolve():

  • 如果我們沒有新增這個方法,反射的時候會新建一個LazyDoubleCheckSingleton對象,並將其返回;
  • 當我們新增這個readResolve()的時候,反射的時候還是會創建一個新的對象,但是,返回的是我們在readResolve()中的定義的返回對象。從而達到了多執行緒安全的目的。

單例模式的反射漏洞

除了反序列化漏洞,單例模式還有反射漏洞。下面介紹一下:
通過反射,能夠破壞單例模式,進而生成多個對象。

先來一個例子,以餓漢式為例:

class HungrySingleton {      private HungrySingleton() {}        private final static HungrySingleton instance = new HungrySingleton();        public static HungrySingleton getInstance() {          return instance;      }  }      public static void main(String[] args) throws Exception {          //測試,餓漢式          Constructor<HungrySingleton> constructor = HungrySingleton.class                  .getDeclaredConstructor();          constructor.setAccessible(true);          HungrySingleton instance = HungrySingleton.getInstance();          HungrySingleton newInstance = constructor.newInstance();          System.out.println(instance);          System.out.println(newInstance);          System.out.println(instance == newInstance);      }

運行一下,就能發現,生成了兩個實例,破壞了單例模式。同樣的情況,也會發生在靜態內部類、懶漢式中。

解決方案

  • 餓漢式、靜態內部類:
    直接改造一下構造器即可,防止生成多個對象。
 private HungrySingleton() {          if (instance != null) {              throw new RuntimeException("禁止反射機制生成實例");          }      }

靜態內部類同理。

  • 懶漢式:
    當你採用懶漢式的時候,關於防止反射攻擊,我是比較悲觀的。當然,解決問題的思路和餓漢式一樣,但是效果卻不盡人意。程式碼演示如下:
    首先,改造構造器。
 private HungrySingleton() {          if (instance != null) {              throw new RuntimeException("單例構造器禁止反射機制調用");          }      }

但是,當你先執行getInstance()方法來生成實例的時候,問題能夠解決,可以當你先通過反射來生成對象的時候,就出問題了:

這時,你的運行結果就如下圖所示:

怎麼辦?有人說,新增一個變數,在構造器中根據變數的值該判斷,但是,這種方式其實沒啥用。因為同樣可以通過反射機制該修改屬性值。
在這裡,再一次想起神奇的枚舉類,既能防止反序列化漏洞,又能防止反射漏洞,推薦大家使用。

單例模式在框架源碼中的使用

jdk中的使用案例

例如Runtime類,使用的就是單例模式的餓漢式(Runtime類在lang包中,在JVM運行的時候就被載入)來實現:

還有Desktop類,使用的就是單例模式的容器模式結合約步鎖來實現的:

Spring中單例模式的應用

Spring單例Bean與單例模式的區別:它們關聯的環境不一樣,單例模式是指在一個JVM進程中僅有一個實例,而Spring單例是指一個Spring Bean容器(ApplicationContext)中僅有一個實例。


當你配置一個bean為單例的時候(默認就是singleton),在獲取對象的時候,spring會讀取判斷為true,然後如果這個對象已經創建好則直接返回,否則就調用方法getEarlySingletonInstance()來創建對象(其源碼為第二張圖片)。

總結

  • 單例模式保證了 系統記憶體中該類只存在一個對象,節省了系統資源,對於一些需要頻繁創建銷毀的對象,使用單例模式可以提高系統性能。
  • 當想實例化一個單例類的時候,必須要記住使用相應的獲取對象的方法,而不是使用new。
  • 單例模式使用的場景:需要頻繁的進行創建和銷毀的對象、創建對象時耗時過多或耗費資源過多(即:重量級對象), 但又經常用到的對象、工具類對象、頻繁訪問資料庫或文件的對象(比如數據源、 session工廠等)。