一起來分析java設計模式之單例

2022-11-07 18:02:53
本篇文章給大家帶來了關於的相關知識, 其中主要介紹了關於設計模式中單例模式的相關內容,單例就一條基本原則是單例物件的類只會被初始化一次,下面一起來看一下,希望對大家有幫助。

程式設計師必備介面測試偵錯工具:

推薦學習:《》

單元素的列舉型別經常成為實現 Singleton 的最佳方法 。

什麼是單例?就一條基本原則,單例物件的類只會被初始化一次。在 Java 中,我們可以說在 JVM 中只存在該類的唯一一個物件範例。在 Android 中,我們可以說在程式執行期間,該類有且僅有一個物件範例。

單例模式的簡單實現步驟:

  • 構造方法私有,保證無法從外部通過 new 的方式建立物件。

  • 對外提供獲取該類範例的靜態方法。

  • 類的內部建立該類的物件,通過第 2 步的靜態方法返回。

按照上述步驟寫下你認為比較嚴謹的單例模式,然後看看你所寫下的單例能否滿足以下條件:

  • 你的單例按需載入嗎?
  • 你的單例執行緒安全嗎?涉及到並行三要素:原子性、可見性、有序性
  • 你的單例暴力反射和序列化安全嗎?

一、餓漢式

//JAVA實現public class SingleTon {    //第三步建立唯一範例
    private static SingleTon instance = new SingleTon();    
    //第一步構造方法私有
    private SingleTon() {
    }    
    //第二步暴露靜態方法返回唯一範例
    public static SingleTon getInstance() {        return instance;
    } 
}//Kotlin實現object SingleTon
登入後複製

優點:設計簡單 ,解決了多執行緒範例化的問題。

缺點:在虛擬機器器載入SingleTon類的時候,將會在初始化階段為類靜態變數賦值,也就是在虛擬機器器載入該類的時候(此時可能並沒有呼叫 getInstance 方法)就已經呼叫了 new SingleTon(); 建立了該物件的範例,之後不管這個範例物件用不用,都會佔據記憶體空間。

二、懶漢式

//JAVA實現public class SingleTon {    //建立唯一範例
    private static SingleTon instance = null;    
    private SingleTon() {
    }    
    public static SingleTon getInstance() {        //延遲初始化 在第一次呼叫 getInstance 的時候建立物件
        if (instance == null) {
            instance = new SingleTon();
        }        return instance;
    } 
}//Kotlin實現class SingleTon private constructor() {    companion object {        private var instance: SingleTon? = null
            get() {                if (field == null) {
                    field = SingleTon()
                }                return field
            }        fun get(): SingleTon{            return instance!!
        }
    }
}
登入後複製

優點:設計也是比較簡單的,和餓漢式不同,當這個Singleton被載入的時候,被static修飾的靜態變數將會被初始化為null,這個時候並不會佔用記憶體,而是當第一次呼叫getInstance方法的時候才會被初始化範例物件,按需建立。

缺點:在單執行緒環境下是沒有問題的,在多執行緒環境下,會產生執行緒安全問題。在有兩個執行緒同時 執行到了 instane == null這個語句,並且都通過了,那他們就會都各自範例化一個物件,這樣就又不是單例了。

如何解決懶漢式在多執行緒環境下的多範例問題?

  • 靜態內部類

    //JAVA實現public class SingleTon {    
        private static class InnerSingleton{        private static SingleTon singleTon  = new SingleTon();
        }    public SingleTon getInstance(){        return InnerSingleton.singleTon;
        }    
        private SingleTon() {
        }
    }//kotlin實現class SingleTon private constructor() {
        companion object {        val instance = InnerSingleton.instance
        }    private object InnerSingleton {        val instance = SingleTon()
        }
    }
    登入後複製
  • 直接同步方法

    //JAVA實現public class SingleTon {    //建立唯一範例
        private static SingleTon instance = null;    
        private SingleTon() {
        }    
        public static synchronized SingleTon getInstance() {        if (instance == null) {
                instance = new SingleTon();
            }        return instance;
        } 
    }//Kotlin實現class SingleTon private constructor() {  companion object {      private var instance: SingleTon? = null
              get() {              if (field == null) {
                      field = SingleTon()
                  }              return field
              }      @Synchronized
          fun get(): SingleTon{          return instance!!
          }
      }
    }
    登入後複製

    優點:加鎖只有一個執行緒能範例該物件,解決了執行緒安全問題。

    缺點:對於靜態方法而言,synchronized關鍵字會鎖住整個 Class,每次呼叫getInstance方法都會執行緒同步,效率十分低下,而且當建立好範例物件之後,也就不必繼續進行同步了。

    備註:此處的synchronized保證了操作的原子性和記憶體可見性。

  • 同步程式碼塊(雙重檢鎖方式DCL)

    //JAVA實現 public class SingleTon {    //建立唯一範例
        private static volatile SingleTon instance = null;    
        private SingleTon() {
        }    
        public static SingleTon getInstance() {        if (instance == null) {
                synchronized (SingleTon.class) {   
                    if (instance == null) {
                        instance = new SingleTon();
                    }
                }
            }        return instance;
        } 
    }//kotlin實現class SingleTon private constructor() {    companion object {        val instance: SingleTon by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
                SingleTon() 
            }
      }
    }
    或者class SingleTon private constructor() {    companion object {        @Volatile private var instance: SingleTon? = null
            fun getInstance() =
                  instance ?: synchronized(this) {
                      instance ?: SingleTon().also { instance = it }
                  }
      }
    }
    登入後複製

    優點:新增了一個同步程式碼塊,在同步程式碼塊中去判斷範例物件是否存在,如果不存在則去建立,這個時候其實就完全可以解決問題了,因為雖然是多個執行緒去獲取範例物件,但是在同一個時間也只會有一個執行緒會進入到同步程式碼塊,那麼這個時候建立好物件之後,其他執行緒即便再次進入同步程式碼塊,由於已經建立好了範例物件,便直接返回即可。但是為什麼還要在同步程式碼塊的上一步再次去判斷instance為空呢?這個是由於當我們建立好範例物件之後,直接去判斷此範例物件是否為空,如果不為空,則直接返回就好了,就避免再次進去同步程式碼塊了,提高了效能。

    缺點:無法避免暴力反射建立物件。

    備註:此處的volatile發揮了記憶體可見性及防止指令重排序作用。

三、列舉實現單例

public enum SingletonEnum {    INSTANCE;    public static void main(String[] args) {        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}
登入後複製

列舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性。(關於Android使用列舉會產生效能問題的說法,這應該是Android 2.x系統之前記憶體緊張的時代了,現在已經Android 13了,相信某些場合列舉所帶來的便利遠遠大於這點所謂的效能影響)

四、如何避免單例模式反射攻擊

以最初的DCL為測試案例,看看如何進行反射攻擊及又如何在一定程度上避免反射攻擊。反射攻擊程式碼如下:

 public static void main(String[] args) {

     SingleTon singleton1 = SingleTon.getInstance();
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
     System.out.println("singleton2.hashCode():" + singleton2.hashCode());
 }
登入後複製

執行結果:

 singleton1.hashCode():1296064247
 singleton2.hashCode():1637070917
登入後複製

通過執行結果發現通過反射破壞了單例。 如何保證反射安全呢?只能以暴制暴,當已經存在範例的時候再去呼叫建構函式直接丟擲異常,對建構函式做如下修改:

  public class SingleTon {     //建立唯一範例
     private static volatile SingleTon instance = null;   
     private SingleTon() {         if (instance != null) {             throw new RuntimeException("單例構造器禁止反射呼叫");
         }
     }   
     public static SingleTon getInstance() {         if (instance == null) {
           synchronized (SingleTon.class) {   
               if (instance == null) {
                   instance = new SingleTon();
               }
           }
       }       return instance;
     } 
 }
登入後複製

此時可防禦反射攻擊,丟擲異常如下:

 java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:45)
 at com.imock.demo.TestUtil.main(TestUtil.java:33)
 Caused by: java.lang.RuntimeException: 單例構造器禁止反射呼叫
 at com.imock.demo.SingleTon.<init>(SingleTon.java:16)
 ... 6 more Exception in thread "main" java.lang.NullPointerException
 at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:49)
 at com.imock.demo.TestUtil.main(TestUtil.java:33) 
 Process finished with exit code 1
登入後複製

然後我們把上述測試程式碼修改如下(調換了singleton1的初始化順序)

 public static void main(String[] args) {
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton2.hashCode():" + singleton2.hashCode());

     SingleTon singleton1 = SingleTon.getInstance(); //調換了位置,在反射之後執行
     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
 }
登入後複製

執行結果:

 singleton2.hashCode():1296064247
 singleton1.hashCode():1637070917
登入後複製

發現此防禦未起到作用。

缺點:

  • 如果反射攻擊發生在正常呼叫getInstance之前,每次反射攻擊都可以獲取單例類的一個範例,因為即使私有構造器中使用了靜態成員(instance) ,但單例物件並沒有在類的初始化階段被範例化,所以防禦程式碼不生效,從而可以通過構造器的反射呼叫建立單例類的多個範例;
  • 如果反射攻擊發生在正常呼叫之後,防禦程式碼是可以生效的;

如何避免序列化攻擊?只需要修改反序列化的邏輯就可以了,即重寫 readResolve() 方法,使其返回統一範例。

   protected Object readResolve() {       return getInstance();
   }
登入後複製

脆弱不堪的單例模式經過重重考驗,進化成了完全體,延遲載入,執行緒安全,反射及序列化安全。簡易程式碼如下:

  • 餓漢模式

    public class SingleTon {    private static SingleTon instance = new SingleTon();    
        private SingleTon() {        if (instance != null) {              throw new RuntimeException("單例構造器禁止反射呼叫");
             }
        }    public static SingleTon getInstance() {        return instance;
        } 
    }
    登入後複製
  • 靜態內部類

    public class SingleTon {    
        private static class InnerStaticClass{        private static SingleTon singleTon  = new SingleTon();
        }    public SingleTon getInstance(){        return InnerStaticClass.singleTon;
        }    
        private SingleTon() {       if (InnerStaticClass.singleTon != null) {           throw new RuntimeException("單例構造器禁止反射呼叫");
           }
        }
    }
    登入後複製
  • 懶漢模式

    public class SingleTon {    //建立唯一範例
        private static SingleTon instance = null;    
        private SingleTon() {        if (instance != null) {              throw new RuntimeException("單例構造器禁止反射呼叫");
            }
        }    
        public static SingleTon getInstance() {        //延遲初始化 在第一次呼叫 getInstance 的時候建立物件
            if (instance == null) {
                instance = new SingleTon();
            }        return instance;
        } 
    }
    登入後複製

    缺點:

    • 如果反射攻擊發生在正常呼叫getInstance之前,每次反射攻擊都可以獲取單例類的一個範例,因為即使私有構造器中使用了靜態成員(instance) ,但單例物件並沒有在類的初始化階段被範例化,所以防禦程式碼不生效,從而可以通過構造器的反射呼叫建立單例類的多個範例;
    • 如果反射攻擊發生在正常呼叫之後,防禦程式碼是可以生效的。

(列舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性,底層實現比如newInstance方法內部判斷列舉拋異常)

推薦學習:《》

以上就是一起來分析java設計模式之單例的詳細內容,更多請關注TW511.COM其它相關文章!