設計模式之單例模式最佳實現方式


  • 單例模式是什么?

    對象在全局只能有一個實例

  • 為什么要使用單例模式?

    • 靜態方法和非靜態方法的區別?

      • 靜態的方法:

        能夠在它的類的任何對象創建之前被訪問,而不必引用任何對象,

        並且static修飾的屬性和方法在整個類中只有一份,可共享,放在方法區中。

      • 非靜態的方法:

        在創建實例對象時,因為屬性的值對於每個對象都各不相同,

        因此在new一個實例時,棧中有個引用地址指向了 ---> 堆中new出來的實例化對象,里面包含對象獨有的屬性和方法,

        再次new實例化有不同屬性值的該對象時,會重新創建一個棧中的引用地址指向 ---> 堆中new出來的實例化對象。

    • 為什么要使用單例模式而非靜態方法?

      • 從面向對象的方式:

        靜態方法是基於對象,單例模式是面向對象,

        如果一個方法不受限於它存在類的實例對象,那么這個方法應該是靜態的,如果確實要使用非靜態的方法,但是只想維持一份實例化對象,只有一個對象可以訪問該方法,那么就需要使用單例模式,

        而且,如果需要在系統運行的時候就加載一些在整個類的生命周期都存在的屬性和方法,那這些類最好是獨一份並且可以共享的,否則new實例化對象時再重新賦值毫無意義且浪費內存,所以需要用靜態方法或單例模式來維持這些獨一份並且可以共享的屬性和方法,靜態方法雖然能同樣解決問題,但是最好的解決方案應該是面向對象的單例模式。

      • 從功能上:

        靜態方法和單例模式都能保證獨一份,

        但是單例模式可以控制單例數量,進行有意義的派生,對實例的創建有更自由的控制。

  • 怎樣實現單例?

    • 類的構造方法私有化,保證其他類不能實例化該對象

    • 本類中實例化該對象,保證該類在全局中存在

    • 創建一個公有的方法,返回的結果是實例化的該對象,供別人使用

1、餓漢式單例

  • 在類創建的時候就直接初始化該類

//代碼實現
public class HungryMan {

   private HungryMan(){}

   /*
   static:
  HUNGRY_MAN需要在調用getInstance()之前就已經被初始化了,只有static的成員才能在沒有創建對象時進行初始化。
  類的static成員在類第一次被使用時初始化后就不會再被初始化,保證了單例。
 
   final:
  保證必須給 HungryMan HUNGRY_MAN 賦值,也就是必須實例化該類
   */
   private static final HungryMan HUNGRY_MAN = new HungryMan();

   public static HungryMan getInstance(){
       return HUNGRY_MAN;
  }
}
  • 在多線程中使用餓漢式單例,8個線程只有一個線程能使用該類的構造方法,說明餓漢式單例是線程安全的

public class HungryMan {
   private HungryMan(){
       System.out.println("餓漢式單例" + Thread.currentThread().getName());
  }
   
   private static final HungryMan HUNGRY_MAN = new HungryMan();
   
   public static HungryMan getInstance(){
       return HUNGRY_MAN;
  }
   
   public static void main(String[] args){
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               HungryMan.getInstance();
          }).start();
      }
  }
}

//輸出結果:餓漢式單例 Thread-0
  • 優點:

    線程安全。

  • 缺點:

    該類進入JVM時,不論該類會不會被使用就直接被實例化,浪費了內存空間。

2、懶漢式單例

  • 使用該類的時候才初始化該類

//代碼實現
public class LazyMan {

   private LazyMan(){}

   private static LazyMan lazyMan;
   
   public static LazyMan getInstance(){
       if (lazyMan == null){
           lazyMan = new LazyMan();
      }
       return lazyMan;
  }
}
  • 在多線程下使用餓漢式單例,8個線程可能有多個線程能使用該類的構造方法,說明懶漢式單例是線程不安全的

public class LazyMan {

   private LazyMan(){
       System.out.println("懶漢式單例 " + Thread.currentThread().getName());
  }

   private static LazyMan lazyMan;
   
   public static LazyMan getInstance(){
       if (lazyMan == null){
           lazyMan = new LazyMan();
      }
       return lazyMan;
  }

   public static void main(String[] args) {
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               LazyMan.getInstance();
          }).start();
      }
  }
}

/*
輸出結果:
懶漢式單例 Thread-0
懶漢式單例 Thread-2
懶漢式單例 Thread-1
*/
  • 優點

    節省內存空間

  • 缺點:

    線程不安全

3、DCL懶漢式(雙重檢查懶漢式)

  • 解決懶漢式線程不安全的問題

//代碼實現
public class DCLLazyMan {

   private DCLLazyMan(){}

   /*
   volatile:
  在原子性上來說是不安全的:
new DCLLazyMan(),創建對象的實例會分解成以下3個指令
•1、分配內存空間
•2、執行構造方法,初始化對象
•3、對象指向內存空間
我們期望按1 2 3 的順序依次完成,但是在內存中會出現指令重排的過程:
假設A類new DCLLazyMan(),是先分配內存空間、占用此空間、初始化對象,也就是132的順序執行,
同時另一個線程中的B類也在new DCLLazyMan(),此時A還沒有完成DCLLazyMan的構造,就會出現不安全的問題。

必須加上volatile關鍵字!
   */
   private volatile static DCLLazyMan lazyMan;

   public static DCLLazyMan getInstance(){
       //第一重檢查
       if (lazyMan == null){
           //加鎖
           synchronized (DCLLazyMan.class){
               //第二重檢查
               if (lazyMan == null){
                   lazyMan = new DCLLazyMan();
              }
          }
      }
       return lazyMan;
  }
}
  • 在多線程中使用DCL懶漢式,8個線程只有一個線程能使用該類的構造方法,說明DCL懶漢式單例是線程安全的

public class DCLLazyMan {

   private DCLLazyMan(){
       System.out.println("DCL懶漢式單例 " + Thread.currentThread().getName());
  }

   private volatile static DCLLazyMan lazyMan;

   public static DCLLazyMan getInstance(){
       if (lazyMan == null){
           synchronized (DCLLazyMan.class){
               if (lazyMan == null){
                   lazyMan = new DCLLazyMan();
              }
          }
      }
       return lazyMan;
  }

   public static void main(String[] args) {
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               DCLLazyMan.getInstance();
          }).start();
      }
  }
}

//輸出結果:DCL懶漢式單例 Thread-0
  • 使用反射破解DCL懶漢式單例的唯一性

public static void main(String[] args) throws Exception {

   //創建對象1和2,通過DCLLazyMan.getInstance()方法
   DCLLazyMan instance1 = DCLLazyMan.getInstance();
   DCLLazyMan instance2 = DCLLazyMan.getInstance();

   //創建對象3,通過反射獲得構造方法的newInstance()方法
   Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   DCLLazyMan instance3 = (DCLLazyMan)constructor.newInstance();

   //輸出結果:instance1和instance2相等嗎? true
   System.out.println("instance1和instance2相等嗎? " + (instance1 == instance2));
   //輸出結果:instance2和instance3相等嗎? false
   System.out.println("instance2和instance3相等嗎? " + (instance2 == instance3));
}

/*
結果分析:
對象1和2,都是通過正常方式創建的,所以指向的都是同一個對象
對象2和3,一個通過正常方式創建,一個通過反射方式創建,已經不是同一個對象了

說明反射可以破壞DCL懶漢式單例的唯一性
*/

4、解決DCL懶漢式被反射破壞唯一性的問題

4.1、第一種方法:再加一重檢查

//修改TCLLazyMan的構造方法為:
private TCLLazyMan(){
   //第三重加鎖判斷
   synchronized (TCLLazyMan.class){
       if (lazyMan != null){
           throw new RuntimeException("不要使用反射破壞類的唯一性");
      }
  }
}


//測試
public static void main(String[] args) throws Exception {

   //創建對象1,通過DCLLazyMan.getInstance()方法
   TCLLazyMan instance1 = TCLLazyMan.getInstance();

   //創建對象2,通過反射獲得構造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();
}

/*
結果分析:
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.RuntimeException: 不要使用反射破壞類的唯一性

說明三重檢測確實可以防止在對象已經被實例化后,再通過反射來創建對象的實例化
*/

但是如果兩個對象均由反射方法創建呢?

public static void main(String[] args) throws Exception {

   //創建對象1和2,通過反射獲得構造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();

   //輸出結果:instance1和instance2相等嗎? false
   System.out.println("instance1和instance2相等嗎? " + (instance1 == instance2));
}

/*
結果分析:
三重檢測防止不了反射破壞類的唯一性
*/

經過代碼分析:三重檢測防止不了反射破壞類的唯一性

4.2、第二種辦法:紅綠燈標識

//修改TCLLazyMan的構造方法為:
private TCLLazyMan(){
   synchronized (TCLLazyMan.class){
       //標識為false,說明之前沒有使用TCLLazyMan的構造方法,也就是沒有被實例化過
       if (ahfndsjbvnc == false){
           //改變標識
           ahfndsjbvnc = true;
      }else{
           throw new RuntimeException("不要使用反射破壞類的唯一性");
      }
  }
}

//測試
public static void main(String[] args) throws Exception {

   //創建對象1和2,通過反射獲得構造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();
}

/*
結果分析:
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.RuntimeException: 不要使用反射破壞類的唯一性

說明這種方法好像可以防止反射創建不同對象
*/

但是如果通過反編譯找到了設置標識的參數,並且修改了它的值呢?

public static void main(String[] args) throws Exception {

   //創建對象1和2,通過反射獲得構造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);

   //通過反射獲得設置標識的屬性
   Field field = TCLLazyMan.class.getDeclaredField("ahfndsjbvnc");
   field.setAccessible(true);

   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   
   //改變屬性的值
   field.set(instance1,false);

   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();

   ////輸出結果:instance1和instance2相等嗎? false
   System.out.println("instance1和instance2相等嗎? " + (instance1 == instance2));
}

/*
結果分析:
設置紅綠燈標識防止不了反射破壞類的唯一性
*/

經過代碼分析:設置紅綠燈標識防止不了反射破壞類的唯一性

4.3、第三種方法:通過枚舉類型

  • 進入newInstance()的源碼

    可知:通過枚舉類型可以防止反射破壞單例模式的唯一性

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
    IllegalArgumentException, InvocationTargetException
{
   if (!override) {
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           Class<?> caller = Reflection.getCallerClass();
           checkAccess(caller, clazz, null, modifiers);
      }
  }
   if ((clazz.getModifiers() & Modifier.ENUM) != 0)
       //如果是枚舉類型,會拋出異常
       throw new IllegalArgumentException("Cannot reflectively create enum objects");
   ConstructorAccessor ca = constructorAccessor;   // read volatile
   if (ca == null) {
       ca = acquireConstructorAccessor();
  }
   @SuppressWarnings("unchecked")
   T inst = (T) ca.newInstance(initargs);
   return inst;
}
  • 測試:

    • 創建一個枚舉類

      public enum EnumSingle {

         INSTANCE;

         public EnumSingle getInstance(){
             return INSTANCE;
        }
      }
    • 通過反射創建對象

      public static void main(String[] args) throws Exception {

         Constructor constructor = EnumSingle.class.getDeclaredConstructor(null);
         constructor.setAccessible(true);
         EnumSingle instance = (EnumSingle)constructor.newInstance();

      }

      /*
      java.lang.NoSuchMethodException: com.hmx.EnumSingle.<init>()
      EnumSingle沒有空的構造函數
      */
    • 分析:

      確實不可以通過反射創建對象,但是所報錯誤並不是源碼中拋出的異常,為什么?

  • 分析所報錯誤並不是源碼中拋出的異常的原因

    第一步:查看target中的class文件:

    public enum EnumSingle {
       INSTANCE;

       //class文件中存在無參構造方法
       private EnumSingle() {
      }

       public EnumSingle getInstance() {
           return INSTANCE;
      }
    }

    第二步:通過cmd命令行編譯class文件:

    進入EnumSingle.class所在位置,輸入javap -p EnumSingle.class命令

    Compiled from "EnumSingle.java"

    public final class com.hmx.EnumSingle extends java.lang.Enum<com.hmx.EnumSingle> {

     public static final com.hmx.EnumSingle INSTANCE;
     private static final com.hmx.EnumSingle[] $VALUES;

     public static com.hmx.EnumSingle[] values();
     public static com.hmx.EnumSingle valueOf(java.lang.String);

     //文件中同樣存在無參構造方法
     private com.hmx.EnumSingle();
     public com.hmx.EnumSingle getInstance();
     static {};
    }

    第三步:使用jad工具把class文件反編譯成Java文件:

    進入EnumSingle.class所在位置,輸入jad -sjava EnumSingle.class命令

    發現EnumSingle類中存在有參構造方法

修改創建對象的代碼

public static void main(String[] args) throws Exception {

   Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   EnumSingle instance = (EnumSingle)constructor.newInstance();

}

/*
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

不能通過反射創建枚舉單例類型的對象
*/

總結

Enum實現單例模式是最佳的方法!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM