【本人禿頂程序員】最簡單的設計模式——單例模式的演進和推薦寫法(Java 版)

←←←←←←←←←←←← 快!點關注c++

前言

以下是以前總結的 C++ 版的;軟件開發經常使用設計模式—單例模式總結(c++版),對比發現 Java 實現的單例模式和 C++ 的在線程安全上仍是有些區別的。web

概念很少說,沒意思,我本身總結就是:sql

有這樣一個類,該類在生命週期內有且只能有一個實例,該類必須本身建立本身的這個惟一實例,該類必須給全部其餘對象提供這一實例(提供全局訪問點),這樣的類就叫單例類。設計模式

簡單的說就是知足三個條件:安全

一、生命週期內有且只能有一個實例多線程

二、本身提供這個獨一無二的實例架構

三、該實例必須是能全局訪問的併發

須要的考慮的細節

進一步,單例類,最好能實現懶加載,隨用隨生成,而不是初始化的時候就生成,提升啓動速度和優化內存。分佈式

還有應該考慮併發環境下的場景,多線程的單例模式實現有什麼難點,回答這個問題,必須先知道Java的內存模型ide

考慮黑客會作反序列化的攻擊

考慮黑客會作反射的攻擊,由於反射能夠訪問私有方法

單線程環境下懶加載的單例

若是程序確認沒有多線程的使用場景,徹底能夠簡單一些寫。

public class NoThreadSafeLazySingleton {
    private static NoThreadSafeLazySingleton lazySingleton = null;

    private NoThreadSafeLazySingleton() {
    }

    public static NoThreadSafeLazySingleton getLazySingleton() {
        if (lazySingleton == null) {
            lazySingleton = new NoThreadSafeLazySingleton();
        }

        return lazySingleton;
    }
}

很簡單,可是隻適用於單線程環境

線程安全的懶加載單例

原理也很簡單,沒什麼可說的,以下示例代碼:

public class ThreadSafeLazySingleton {
    private static volatile ThreadSafeLazySingleton lazySingleton = null;

    private ThreadSafeLazySingleton() {
    }

    public static ThreadSafeLazySingleton getLazySingleton() {
        if (lazySingleton == null) {
            synchronized (ThreadSafeLazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new ThreadSafeLazySingleton();
                }
            }
        }

        return lazySingleton;
    }
}

主要是注意 volatile 關鍵字的使用,不然這種所謂雙重檢查的線程安全的單例是有 bug 的。

靜態內部類方案

在某些狀況中,JVM 隱含了同步操做,這些狀況下就不用本身再來進行同步控制了。這些狀況包括:

  • 由靜態初始化器(在靜態字段上或static{}塊中的初始化器)初始化數據時
  • 訪問final字段時
  • 在建立線程以前建立對象時
  • 線程能夠看見它將要處理的對象時

在靜態內部類裏去建立本類(外部類)的對象,這樣只要不使用這個靜態內部類,那就不建立對象實例,從而同時實現延遲加載和線程安全。

public class Person {
    private String name;
    private Integer age;

    private Person() {
    }

    private Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // 在靜態內部類裏去建立本類(外部類)的對象
    public static Person getInstance() {
        return Holder.instatnce;
    }

    // 靜態內部類至關於外部類 Person 的 static 域,它的對象與外部類對象間不存在依賴關係,所以可直接建立。
    // 由於靜態內部類至關於其外部類 Person 的靜態成員,因此在第一次被使用的時候才被會裝載,且只裝載一次。
    private static class Holder {
        // 內部類的對象實例 instatnce ,是綁定在外部 Person 對象實例中的
        // 靜態內部類中能夠定義靜態方法,在靜態方法中只可以引用外部類中的靜態成員方法或者成員變量,好比 new Person
        // 使用靜態初始化器來實現線程安全的單例類,它由 JVM 來保證線程安全性。
        private static final Person instatnce = new Person("John", 31);
    }
}

靜態內部類至關於外部類 Person 的 static 域(靜態成員),它的對象與外部類對象間不存在依賴關係,所以可直接建立。

既然,靜態內部類至關於其外部類 Person 的靜態成員,因此在第一次被使用的時候才被會裝載,且只裝載一次,實現了懶加載和單例。

並且,使用靜態初始化器來實現單例類,是線程安全的,由於由 JVM 來保證線程安全性

客戶端調用

Person person = Person.getInstance();

該方案實現了,線程安全的單例 + 懶加載的單例,可是並不能防反序列化攻擊,須要額外的加以約束。

反序列化攻擊單例類

其實這個 case 不必說太多,知道就行,由於哪裏就這麼巧,一個能序列化的類(實現了Serializable/Externalizable接口的類),就偏偏是單例的呢?

看下面例子,把 Person 類改造爲能序列化的類,而後用反序列攻擊單例

public class SerializationTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = Person.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person"));
        objectOutputStream.writeObject(person);

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person"));
        Person person1 = (Person) objectInputStream.readObject();

        System.out.println(person == person1); // false
    }
}

比較兩個 person 實例地址,是 false,說明生成了兩個對象,違背了單例類的初衷,那麼爲了能在序列化過程仍能保持單例的特性,能夠在Person類中添加一個readResolve()方法,在該方法中直接返回Person的單例對象

public Object readResolve() {
        return Holder.instatnce;
    }

原理是當從 I/O 流中讀取對象時,ObjectInputStream 類裏有 readResolve() 方法,該方法會被自動調用,期間通過種種邏輯,最後會調用到可序列化類裏的 readResolve()方法,這樣能夠用 readResolve() 中返回的單例對象直接替換在反序列化過程當中建立的對象,實現單例特性。

也就是說,不管如何,反序列化都會額外建立對象,只不過使用 readResolve() 方法能夠替換之。

反射攻擊單例類

直接看例子,作法很簡單,經過 Java 的反射機制,看看能不能拿到單例類的私有構造器,而且改變構造器的訪問屬性

public class ReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        Person person = Person.getInstance();

        Class clazz = Class.forName("com.dashuai.D13Singleton.Person");
        Constructor constructor = clazz.getDeclaredConstructor();
//        constructor.setAccessible(true);
        Person person1 = (Person) constructor.newInstance();

        System.out.println(person == person1); // false
    }
}

運行拋出了異常:

可是,若是把註釋的行打開,就不會出錯,且打印 false。

網上有一些解決方案,好比在構造器里加判斷,若是二次調用就拋出異常,其實也沒從根本上解決問題。

解決全部問題的方案——枚舉實現單例類

目前公認的最佳方案,代碼極少,線程安全,防止反射和序列化攻擊

public enum EnumSingleton {
    ENUM_SINGLETON;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
////////////////////////調用
EnumSingleton.ENUM_SINGLETON.setName("dashuai");
System.out.println(EnumSingleton.ENUM_SINGLETON.getName());

全部的變量都是單例的。至於爲何,能夠經過反編譯工具查看枚舉的源碼。能夠安裝 idea 的 jad 插件,會發現就是按照單例模式設計的。

享元模式和單例模式的異同

享元模式是對象級別的, 也就是說在多個使用到這個對象的地方都只須要使用這一個對象便可知足要求。

單例模式是類級別的, 就是說這個類必須只能實例化出來一個對象。

能夠這麼說, 單例是享元的一種特例, 設計模式不用拘泥於具體代碼, 代碼實現可能有n多種方式, 而單例能夠看作是享元的實現方式中的一種, 他比享元模式更加嚴格的控制了對象的惟一性

使用單例的場景和條件是什麼?

一、單例類只能有一個實例。

二、單例類必須本身建立本身的惟一實例。

三、單例類必須給全部其餘對象提供這一實例。


讀者福利:

分享免費學習資料

針對於還會準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)
爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!

資料領取方式:加入粉絲羣963944895,私信管理員便可免費領取