第三天:?jiǎn)卫J?/h2>

1. 單例模式

單例模式(Singleton Pattern) 是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式,旨在確保一個(gè)類只有一個(gè)實(shí)例,并提供全局訪問(wèn)點(diǎn)來(lái)獲取這個(gè)實(shí)例。單例模式在某些情況下非常有用,比如需要共享資源或管理全局狀態(tài)的場(chǎng)景。

1.1 單例模式的要點(diǎn)

  1. 唯一實(shí)例:類只能有一個(gè)實(shí)例。
  2. 全局訪問(wèn)點(diǎn):提供一個(gè)全局的訪問(wèn)點(diǎn),用于獲取該唯一實(shí)例。
  3. 防止外部創(chuàng)建實(shí)例:通過(guò)將構(gòu)造方法設(shè)為私有,避免其他類直接創(chuàng)建對(duì)象。

這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問(wèn)其唯一的對(duì)象的方式,可以直接訪問(wèn),不需要實(shí)例化該類的對(duì)象。

1.2 單例模式的實(shí)現(xiàn)

單例設(shè)計(jì)模式分類兩種:餓漢式、懶漢式。

餓漢式:類加載就會(huì)導(dǎo)致該單實(shí)例對(duì)象被創(chuàng)建。

餓漢式有三種實(shí)現(xiàn)方式:靜態(tài)常量方式、靜態(tài)代碼塊方式、枚舉形式。

懶漢式:類加載不會(huì)導(dǎo)致該單實(shí)例對(duì)象被創(chuàng)建,而是首次使用該對(duì)象時(shí)才會(huì)創(chuàng)建。

懶漢式有四種實(shí)現(xiàn)方式:線程不安全方式、線程安全方式、雙重檢查鎖方式(線程安全方式優(yōu)化)、靜態(tài)內(nèi)部類方式(線程安全方式優(yōu)化)

1.3 餓漢式單例模式

1.3.1 靜態(tài)常量方式

靜態(tài)初始化,類加載時(shí)創(chuàng)建實(shí)例

  • 優(yōu)點(diǎn):這種寫法比較簡(jiǎn)單,就是 在類加載的時(shí)候就完成實(shí)例化。避免了線程同步問(wèn)題。
  • 缺點(diǎn):在類加載時(shí)就完成實(shí)例化,沒(méi)有達(dá)到懶加載的效果。如果從未使用過(guò)這個(gè)實(shí)例,則會(huì) 造成內(nèi)存的浪費(fèi)。

類圖:

image.png
public class Singleton {
    // 靜態(tài)初始化,類加載時(shí)創(chuàng)建實(shí)例
    private static final Singleton instance = new Singleton();

    // 私有化構(gòu)造函數(shù)
    private Singleton() {}

    // 提供全局訪問(wèn)點(diǎn)
    public static Singleton getInstance() {
        return instance;
    }
}

1.3.2 靜態(tài)代碼塊方式

將類實(shí)例化的過(guò)程放在了靜態(tài)代碼塊中,也是在類加載的時(shí)候,就執(zhí)行靜態(tài)代碼塊中的代碼,初始化類的實(shí)例。

  • 優(yōu)缺點(diǎn)和上面的方式是一樣的。

類圖:

image.png
public class Singleton {
    // 靜態(tài)初始化,類加載時(shí)創(chuàng)建實(shí)例
    private static final Singleton instance;

    //在靜態(tài)代碼塊中進(jìn)行賦值
    static {
        instance = new Singleton();
    }

    // 私有化構(gòu)造函數(shù)
    private Singleton() {
    }

    // 提供全局訪問(wèn)點(diǎn)
    public static Singleton getInstance() {
        return instance;
    }
}

1.3.3 枚舉形式

通過(guò)枚舉實(shí)現(xiàn)單例,是最推薦的實(shí)現(xiàn)方式,因?yàn)樗?jiǎn)潔、線程安全,并且可以防止序列化和反射攻擊。這種方式是Effective Java作者Josh Bloch 提倡的方式。

  • 優(yōu)點(diǎn):線程安全,防止反序列化破壞單例,簡(jiǎn)潔優(yōu)雅。
  • 缺點(diǎn):不適合需要延遲加載的情況。

類圖:

image.png
public enum Singleton {
    INSTANCE;
}

1.4 懶漢式單例模式

1.4.1 線程不安全方式

該方式起到了懶加載的效果,但是只能在單線程下使用。如果在多線程下,一個(gè)線程進(jìn)入了 if (singleton == null) 判斷語(yǔ)句塊,還未來(lái)得及往下執(zhí)行,另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例。所以 在多線程環(huán)境下不可使用這種方式。在實(shí)際開(kāi)發(fā)中,不要使用這種方式.

  • 優(yōu)點(diǎn):延遲加載實(shí)例,節(jié)省資源。
  • 缺點(diǎn):在多線程環(huán)境下可能會(huì)創(chuàng)建多個(gè)實(shí)例,線程不安全。

類圖:

image.png
public class Singleton {
    // 持有唯一實(shí)例,使用時(shí)再初始化
    private static Singleton instance;

    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        if (instance == null) {
            // 延遲創(chuàng)建實(shí)例
            instance = new Singleton();
        }
        return instance;
    }
}

1.4.2 線程安全方式

該方式也起到了懶加載效果,同時(shí)又解決了線程安全問(wèn)題。但是在 getInstance() 方法上添加了 synchronized 關(guān)鍵字,導(dǎo)致該方法的執(zhí)行效率特別低。從上面代碼我們可以看出,其實(shí)就是在初始化 instance 的時(shí)候才會(huì)出現(xiàn)線程安全問(wèn)題,一旦初始化完成就不存在了。

  • 優(yōu)點(diǎn):延遲加載實(shí)例,節(jié)省資源。
  • 缺點(diǎn):執(zhí)行效率特別低。

類圖:


image.png
public class Singleton {
    // 持有唯一實(shí)例,使用時(shí)再初始化
    private static Singleton instance;

    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 提供獲取實(shí)例的方法,并且對(duì)該方法加鎖
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            // 延遲創(chuàng)建實(shí)例
            instance = new Singleton();
        }
        return instance;
    }
}

1.4.3 雙重檢查鎖方式(線程安全方式優(yōu)化)

對(duì)于 getInstance() 方法來(lái)說(shuō),絕大部分的操作都是讀操作,讀操作是線程安全的。所以我們沒(méi)必讓每個(gè)線程必須持有鎖才能調(diào)用該方法,我們需要調(diào)整加鎖的時(shí)機(jī),通過(guò)雙重檢查鎖方式保證線程安全,同時(shí)確保懶加載。

優(yōu)點(diǎn):在保證線程安全的前提下,減少了鎖的使用頻率,提高了性能。
缺點(diǎn):實(shí)現(xiàn)相對(duì)復(fù)雜,并且由于 Java 內(nèi)存模型(JMM) 的原因,需要使用 volatile 關(guān)鍵字來(lái)確保正確的實(shí)例化。

類圖:


image.png
public class Singleton {
    // 持有唯一實(shí)例,使用時(shí)再初始化,volatile 保證可見(jiàn)性和有序性
    private static volatile Singleton instance;

    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // 延遲創(chuàng)建實(shí)例
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

1.4.4 靜態(tài)內(nèi)部類方式(線程安全方式優(yōu)化)

靜態(tài)內(nèi)部類單例模式中實(shí)例由內(nèi)部類創(chuàng)建,由于 JVM 在加載外部類的過(guò)程中, 是不會(huì)加載靜態(tài)內(nèi)部類的, 只有內(nèi)部類的屬性/方法被調(diào)用時(shí)才會(huì)被加載, 并初始化其靜態(tài)屬性。靜態(tài)屬性由于被 static 修飾,保證只被實(shí)例化一次,并且嚴(yán)格保證實(shí)例化順序。

  • 優(yōu)點(diǎn):避免了線程不安全,利用靜態(tài)內(nèi)部類特點(diǎn)實(shí)現(xiàn)延遲加載,效率高。
  • 缺點(diǎn):無(wú)明顯缺點(diǎn),被認(rèn)為是單例模式的最佳實(shí)踐之一。

類圖:


image.png
public class Singleton {
    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 靜態(tài)內(nèi)部類,持有單例實(shí)例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        // 只有在調(diào)用時(shí)才會(huì)加載 SingletonHolder 類
        return SingletonHolder.INSTANCE;
    }
}

1.5 單例模式破壞方式

盡管單例模式的目的是確保一個(gè)類只有一個(gè)實(shí)例,但在某些特殊情況下,可以通過(guò)一些方法破壞單例模式的約束。以下是幾種常見(jiàn)的破壞單例模式的方式以及對(duì)應(yīng)的解決方案(以靜態(tài)內(nèi)部類方式為例)。

1.5.1 反射機(jī)制

Java 的反射機(jī)制允許訪問(wèn)私有構(gòu)造函數(shù),即使構(gòu)造函數(shù)是私有的,也可以通過(guò)反射調(diào)用,從而創(chuàng)建新的實(shí)例。

1.5.1.1 破壞方式

使用反射可以繞過(guò)單例類的私有構(gòu)造方法,強(qiáng)行創(chuàng)建新的實(shí)例。

public class Client {
    public static void main(String[] args) throws Exception {
        // 創(chuàng)建 Singleton 類的對(duì)象
        Singleton instance1 = Singleton.getInstance();

        // 使用反射創(chuàng)建新實(shí)例
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        // 使用反射創(chuàng)建新實(shí)例
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();

        // 判斷獲取到的兩個(gè)實(shí)例是否是同一個(gè)對(duì)象,輸出:false,表示實(shí)例被破壞
        System.out.println(instance1 == instance2);
    }
}

1.5.1.2 解決方案

可以在構(gòu)造函數(shù)中加上檢查,防止通過(guò)反射創(chuàng)建多個(gè)實(shí)例。

public class Singleton {
    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
        // 防止反射調(diào)用,防止反射攻擊的額外防御
        if (SingletonHolder.INSTANCE != null) {
            throw new RuntimeException("Cannot create instance, use getInstance()");
        }
    }

    // 靜態(tài)內(nèi)部類,持有單例實(shí)例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        // 只有在調(diào)用時(shí)才會(huì)加載 SingletonHolder 類
        return SingletonHolder.INSTANCE;
    }
}

這種方式可以在實(shí)例化過(guò)程中,檢測(cè)是否已有實(shí)例,如果存在,則拋出異常,防止反射破壞單例。

1.5.2 序列化

在使用 Serializable 接口的情況下,反序列化會(huì)創(chuàng)建新的對(duì)象,破壞單例模式。

1.5.2.1 破壞方式

即使通過(guò) getInstance() 方法獲得了唯一實(shí)例,但序列化再反序列化會(huì)導(dǎo)致產(chǎn)生一個(gè)新的實(shí)例。

public class Client {
    public static void main(String[] args) throws Exception {
        // 創(chuàng)建 Singleton 類的對(duì)象
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 將對(duì)象寫入文件
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance1.ser"));
        out.writeObject(instance1);
        out.close();
        // 從文件中讀取對(duì)象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance1.ser"));
        Singleton instance = (Singleton) in.readObject();
        in.close();

        // 判斷獲取到的兩個(gè)實(shí)例是否是同一個(gè)對(duì)象,輸出:false,表示實(shí)例被破壞
        System.out.println(instance == instance2);
    }
}

1.5.2.2 解決方案

可以通過(guò)實(shí)現(xiàn) readResolve() 方法,在反序列化時(shí)確保返回已有實(shí)例。

public class Singleton implements Serializable {
    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 靜態(tài)內(nèi)部類,持有單例實(shí)例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        // 只有在調(diào)用時(shí)才會(huì)加載 SingletonHolder 類
        return SingletonHolder.INSTANCE;
    }

    protected Object readResolve() {
        // 返回唯一的實(shí)例
        return SingletonHolder.INSTANCE;
    }
}

readResolve() 方法在反序列化時(shí)會(huì)替換生成的實(shí)例,保證返回的是已有的單例對(duì)象。

1.5.3 克隆

如果類實(shí)現(xiàn)了 Cloneable 接口,并重寫了 clone() 方法,克隆操作會(huì)生成一個(gè)新的實(shí)例,破壞單例模式。

1.5.3.1 破壞方式

通過(guò)克隆創(chuàng)建新實(shí)例。

public class Client {
    public static void main(String[] args) throws Exception {
        // 創(chuàng)建 Singleton 類的對(duì)象
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = (Singleton) instance1.clone();

        // 判斷獲取到的兩個(gè)實(shí)例是否是同一個(gè)對(duì)象,輸出:false,表示實(shí)例被破壞
        System.out.println(instance1 == instance2);
    }
}

1.5.3.2 解決方案

通過(guò)重寫 clone() 方法,禁止克隆操作。

public class Singleton implements Cloneable {
    // 私有化構(gòu)造函數(shù),防止外部創(chuàng)建實(shí)例
    private Singleton() {
    }

    // 靜態(tài)內(nèi)部類,持有單例實(shí)例
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 提供獲取實(shí)例的方法
    public static Singleton getInstance() {
        // 只有在調(diào)用時(shí)才會(huì)加載 SingletonHolder 類
        return SingletonHolder.INSTANCE;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton cannot be cloned");
    }
}

這種方式阻止了單例對(duì)象被克隆,從而保證唯一性。

1.6 JDK源碼解析-Runtime類

從下面源代碼中可以看出 Runtime 類使用的是懶漢式單例模式(靜態(tài)常量方式)來(lái)實(shí)現(xiàn)單例模式的。

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class {@code Runtime} are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the {@code Runtime} object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容