單例模式,你真的寫對了嗎?

看公司代碼的時候發現項目中單例模式應用挺多的,而且發現的兩處單例模式用的仍是不一樣的方式實現的,那麼單例模式到底有幾種寫法呢?單例模式看似很簡單,可是實際寫起來卻問題多多java

本文大綱

  • 什麼是單例模式
  • 餓漢式建立單例對象
  • 懶漢式建立單例對象
  • 單例模式的優缺點
  • 單例模式的應用場景

什麼是單例模式

確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例,而且有兩種建立方式,一種是餓漢式建立,另一種是懶漢式建立git

餓漢式建立單例模式

餓漢式建立就是在類加載時就已建立好對象,而不是在須要時在建立對象github

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有構造函數,不能被外部所訪問
     */
    private HungrySingleton() {}

    /**
     * 返回單例對象
     * */
    public static HungrySingleton getHungrySingleton() {
        return hungrySingleton;
    }
}
複製代碼

說明:面試

  • 構造函數私有化,保證外部不能調用構造函數建立對象,建立對象的行爲只能由這個類決定
  • 只能經過getHungrySingleton方法獲取對象
  • HungrySingleton對象已經建立完成【在類加載時建立】

缺點:安全

  • 若是getHungrySingleton一直沒有被使用到,有點浪費資源

優勢:bash

  • ClassLoad保證線程安全

懶漢式建立單例模式

懶漢式建立就是在第一次須要該對象時在建立函數

  • 存在錯誤的懶漢式建立單例對象
    根據定義很容易在上面餓漢式的基礎上進行修改工具

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
    
        /**
         * 構造函數私有化
         * */
        private LazySingleton() {
        }
    
        private static LazySingleton getLazySingleton() {
            if (lazySingleton == null) {
                return new LazySingleton();
            }
          
            return lazySingleton;
        }
    }
    複製代碼

    說明:性能

    • 構造函數私有化
    • 當須要時【getLazySingleton方法調用時】才建立 嗯,好像沒什麼問題,可是當有多個線程同時調用getLazySingleton方法時,此時恰好對象沒有初始化,兩個線程同時經過lazySingleton == null的校驗,將會建立兩個LazySingleton對象。必須搞點手段使getLazySingleton方法是線程安全的
  • synchronizeLock
    很容易想到使用synchronizeLock對方法進行加鎖
    使用synchronize測試

    public class LazySynchronizeSingleton {
        private static LazySynchronizeSingleton lazySynchronizeSingleton= null;
      
        /**
         * 構造函數私有化
         * */
        private LazySynchronizeSingleton() {
        }
    
        public synchronized static LazySynchronizeSingleton getLazySynchronizeSingleton() {
            if (lazySynchronizeSingleton == null) {
                lazySynchronizeSingleton = new LazySynchronizeSingleton();
            }
          
            return lazySynchronizeSingleton;
        }
    }
    複製代碼

    使用Lock

    public class LazyLockSingleton {
        private static LazyLockSingleton lazyLockSingleton = null;
    
        /**
        * 鎖
        **/
        private static Lock lock = new ReentrantLock();
    
        /**
         * 構造函數私有化
         * */
        private LazyLockSingleton() {
        }
    
        public static LazyLockSingleton getLazyLockSingleton() {
            try {
                lock.lock();
                if (lazyLockSingleton == null) {
                    lazyLockSingleton = new LazyLockSingleton();
                }
            } finally {
                lock.unlock();
            }
          
            return lazyLockSingleton;
        }
    }
    複製代碼

    這兩種方式雖然保證了線程安全,可是性能較差,由於線程不安全主要是由這段代碼引發的:

    if (lazyLockSingleton == null) {
      lazyLockSingleton = new LazyLockSingleton();
    }
    複製代碼

    給方法加鎖不管對象是否已經初始化都會形成線程阻塞。若是對象爲null的狀況下才進行加鎖,對象不爲null的時候則不進行加鎖,那麼性能將會獲得提高,雙重鎖檢查能夠實現這個需求

  • 雙重鎖檢查
    在加鎖以前先判斷lazyDoubleCheckSingleton == null是否成立,若是不成立直接返回建立好的對象,成立在加鎖

    public class LazyDoubleCheckSingleton {
        /**
         * 使用volatile進行修飾,禁止指令重排
         * */
        private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    
        /**
         * 構造函數私有化
         * */
        private LazyDoubleCheckSingleton() {
        }
    
        public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() {
            if (lazyDoubleCheckSingleton == null) {
                synchronized (LazyDoubleCheckSingleton.class) {
                    if (lazyDoubleCheckSingleton == null) {
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
          
            return lazyDoubleCheckSingleton;
        }
    }
    複製代碼

    說明:

    • 爲何須要對lazyDoubleCheckSingleton添加volatile修飾符 由於lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();不是原子性的,分爲三步:
      • lazyDoubleCheckSingleton分配內存
      • 調用構造函數進行初始化
      • lazyDoubleCheckSingleton對象指向分配的內存【執行完這步lazyDoubleCheckSingleton將不爲null

    爲了提升程序的運行效率,編譯器會進行一個指令重排,步驟2和步驟三進行了重排,線程1先執行了步驟一和步驟三,執行完後,lazyDoubleCheckSingleton不爲null,此時線程2執行到if (lazyDoubleCheckSingleton == null),線程2將可能直接返回未正確進行初始化的lazyDoubleCheckSingleton對象。出錯的緣由主要是lazyDoubleCheckSingleton未正確初始化完成【寫】,可是其餘線程已經讀取lazyDoubleCheckSingleton的值【讀】,使用volatile能夠禁止指令重排序,經過內存屏障保證寫操做以前不會調用讀操做【執行if (lazyDoubleCheckSingleton == null)

    缺點:

    • 爲了保證線程安全,代碼不夠優雅過於臃腫
  • 靜態內部類

    public class LazyStaticSingleton {
        /**
         * 靜態內部類
         * */
        private static class LazyStaticSingletonHolder {
            private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();
        }
    
        /**
         * 構造函數私有化
         * */
        private LazyStaticSingleton() {
        }
    
        public static LazyStaticSingleton getLazyStaticSingleton() {
            return LazyStaticSingletonHolder.lazyStaticSingleton;
        }
    }
    複製代碼

    靜態內部類在調用時纔會進行初始化,所以是懶漢式的,LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();看似是餓漢式的,可是隻有調用getLazyStaticSingleton時纔會進行初始化,線程安全由ClassLoad保證,不用思考怎麼加鎖

前面幾種方式實現單例的方式雖然各有優缺點,可是基本實現了單例線程安全的要求。可是總有人看不慣單例模式勤儉節約的做用,對它進行攻擊。對它進行攻擊無非就是建立不僅一個類,java中建立對象的方式有newclone、序列化、反射。構造函數私有化不可能經過new建立對象、同時單例類沒有實現Cloneable接口沒法經過clone方法建立對象,那剩下的攻擊只有反射攻擊和序列化攻擊了
反射攻擊:

public class ReflectAttackTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //靜態內部類
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        //經過反射建立LazyStaticSingleton
        Constructor<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
        //打印結果爲false,說明又建立了一個新對象
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

        //synchronize
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
        lazySynchronizeSingletonConstructor.setAccessible(true);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getConstructor();
        lazyLockSingletonConstructor.setAccessible(true);
        LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //雙重鎖檢查
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getConstructor();
        lazyDoubleCheckSingletonConstructor.setAccessible(true);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
    }
}
複製代碼

基於靜態內部類和基於synchronize加鎖建立單例對象的方式均可以經過反射的方式建立新對象,存在反射攻擊,其他幾種建立單例對象的方式使用反射建立新對象將會報錯。針對存在的反射攻擊根據網上提供的思路在搶救一下,搶救姿式以下:

private LazySynchronizeSingleton() {
      //flag爲線程間共享,進行加鎖控制
      synchronized (LazySynchronizeSingleton.class) {
          if (flag == false) {
              flag = !flag;
          } else {
              throw new RuntimeException("單例模式被攻擊");
          }
      }
  }
複製代碼

構造函數只能調用一次,調用第二次將拋出異常,經過flag來判斷構造函數是否已經被調用過一次了。可是咱們仍能夠經過反射修改flag的值:

//調用反射前將flag設置爲false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);
複製代碼

搶救失敗,你可能想經過final修飾禁止修改,可是反射能夠先去除final,在加上final修改值,對於反射攻擊,無力迴天,只能選擇不適用存在反射攻擊的單例建立方式

反序列化攻擊:

public class SerializableAttackTest {
    public static void main(String[] args) {
        //懶漢式
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
        //序列化
        byte[] serialize = SerializationUtils.serialize(hungrySingleton);
        //反序列化
        HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
        System.out.println(hungrySingleton == hungrySingleton1);

        //雙重鎖
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
        LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //synchronie
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //靜態內部類
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

    }
}
複製代碼

打印結果都爲false,都存在反序列化攻擊
對於反序列化攻擊,仍是有有效的搶救方式的,搶救姿式以下:

private Object readResolve() {
    return lazySynchronizeSingleton;
}
複製代碼

添加readResolve方法並返回建立的單例對象,至於搶救的原理,能夠經過跟進SerializationUtils.deserialize的代碼可知
上述實現單例對象的方式既要考慮線程安全、又要考慮攻擊,而經過枚舉建立單例對象徹底不用擔憂這些問題

  • 枚舉
    public enum EnumSingleton {
        INSTANCE;
    
        public static EnumSingleton getEnumSingleton() {
            return INSTANCE;
        }
    }
    複製代碼
    代碼實現也至關優美,總共才8行代
    實現原理:枚舉類的域(field)實際上是相應的enum類型的一個實例對象
    能夠參考:implementing-singleton-with-an-enum-in-java
    枚舉攻擊測試:
    public class EnumAttackTest {
      public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
          EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton();
          //序列化攻擊
          byte[] serialize4 = SerializationUtils.serialize(enumSingleton);
          EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4);
          System.out.println(enumSingleton == enumSingleton2);
          
          //反射攻擊
          Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getConstructor();
          enumSingletonConstructor.setAccessible(true);
          EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance();
          System.out.println(enumSingleton == enumSingleton1);
      }
    }
    複製代碼
    反射攻擊將會拋出異常,序列化攻擊對它無效,打印結果爲true,用枚舉建立單例對象真的是無懈可擊

單例模式的優勢

  • 只建立了一個實例,節省內存開銷
  • 減小了系統的性能開銷,建立對象回收對象對性能都有必定的影響
  • 避免對資源的多重佔用
  • 在系統設置全局的訪問點,優化和共享資源優化

總結一下就是節約資源、提高性能

單例模式的缺點

  • 不適用於變化的對象
  • 單例模式中沒有抽象層,擴展有困難
  • 與單一原則衝突。一個類應該只實現一個邏輯,而不關心它是否單例,是否是單例應該由業務決定

單例模式的應用場景

  • Spring IOC默認使用單例模式建立bean
  • 建立對象須要消耗的資源過多時
  • 須要定義大量的靜態常量和靜態方法的環境,好比工具類【感受是最多見應用場景】

小結

總共介紹了六種正確建立單例對象的方式,推薦使用餓漢式建立單例對象的方式,若是對資源使用有要求,則推薦使用靜態內部類【注意反序列化攻擊】,其餘方式在保證線程安全的同時對性能將會有影響。枚舉類實際上是很是不錯的,線程安全、不存在反射攻擊和反序列化攻擊,可是感受這種建立單例方式應用較少,公司代碼中使用的是雙重鎖檢查和靜態內部類【存在反序列化攻擊】建立單例方式,甚至以前出去面試時面試官讓寫一個單例,我使用的是枚舉方式,面試官都不知道有這種方式

最後附:完整例子代碼+測試代碼

歡迎forkstar

相關文章
相關標籤/搜索