首頁  >  文章  >  Java  >  一起來分析java設計模式之單例

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

WBOY
WBOY轉載
2022-11-07 16:56:26957瀏覽

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

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

推薦學習:《java影片教學

單元素的枚舉類型經常成為實作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视频教程

以上是一起來分析java設計模式之單例的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除