天天看點

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

目錄

​​一、單例模式​​

​​二、餓漢模式和懶漢模式​​

​​1、餓漢模式,線程安全​​

​​2、懶漢模式​​

​​懶漢模式1,線程不安全(不常用)​​

​​懶漢模式2,線程安全(不常用)​​

​​懶漢模式3,線程安全,雙重校驗(不常用)​​

​​懶漢模式4,線程安全,雙重校驗,volatile可見性,實作較為複雜​​

​​懶漢模式5,線程安全,靜态内部類​​

​​懶漢模式6,線程安全,靜态内部類,防止反射​​

​​3、readResolve方法​​

​​序列化測試​​

​​ObjectOutputStream是怎麼校驗readResolve()的​​

​​概括一下ObjectOutputStream().readObject()的整個大緻流程​​

​​4、枚舉方式,線程安全(不常用)​​

​​三、項目位址​​

一、單例模式

單例對象是一種常用的設計模式。這種模式涉及到一個單一的類,該類負責建立自己的對象,同時確定隻有單個對象被建立。這個類提供了一種通路其唯一的對象的方式,可以直接通路,不需要執行個體化該類的對象。

在Java應用中,單例對象能保證在一個JVM中,該對象隻有一個執行個體存在。這樣的模式有幾個好處:

1、某些類建立比較頻繁,對于一些大型的對象,這是一筆很大的系統開銷。

2、省去了new操作符,降低了系統記憶體的使用頻率,減輕GC壓力。

3、有些類如交易所的核心交易引擎,控制着交易流程,如果該類可以建立多個的話,系統完全亂了。

單例模式的特點總結就是

  • 1、單例類隻能有一個執行個體。
  • 2、單例類必須自己建立自己的唯一執行個體。
  • 3、單例類必須給所有其他對象提供這一執行個體。

二、餓漢模式和懶漢模式

餓漢式和懶漢式的差別,就是懶漢式比較懶,不先加載執行個體;餓漢式不管使用者是否要使用該類的對象,就先建立好了一個執行個體放在記憶體中。

1、餓漢模式,線程安全

對象執行個體建立

package cn.zygxsq.design.module.singletonPattern.hungryMode;

/**
 * 懶漢模式
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class Singleton {
    /* 持有私有靜态執行個體,防止被引用*/
    private static Singleton instance = new Singleton();

    /* 私有構造方法,防止被執行個體化 */
    private Singleton() {
    }

    /* 靜态工程方法,傳回Singleton執行個體 */
    public static Singleton getInstance() {
        return instance;                                                                   // 原文:javascript:void(0)
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return instance;
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

test調用Singleton執行個體中的方法

package cn.zygxsq.design.module.singletonPattern.hungryMode;

/**
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonTest {
    public static void main(String[] args) {
        //不合法的構造函數
        //編譯時錯誤:構造函數 Singleton() 是不可見的
        //Singleton instance = new Singleton();                              // 原文:javascript:void(0)

        //擷取唯一可用的對象
        Singleton instance = Singleton.getInstance();

        //調用方法
        instance.dosomething();
    }
}      

執行結果 

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

上述方法就是實作單例模式的其中一種(餓漢模式),這種方式比較常用,但是在類中不管使用者是否要使用該類的對象,就先建立好了一個執行個體放在記憶體中,這就比較浪費記憶體。

優點:沒有加鎖,執行效率會提高。

缺點:類加載時就初始化,浪費記憶體。

它基于 classloader 機制避免了多線程的同步問題

我們試着不先建立對象的執行個體,到用的時候才去建立,這就需要用到懶漢模式

2、懶漢模式

懶漢模式1,線程不安全(不常用)

對象執行個體建立

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy1;

/**
 * 懶漢模式1,線程不安全
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy1 {
    /* 持有私有靜态執行個體,防止被引用,此處指派為null,目的是實作延遲加載 */
    private static SingletonLazy1 instance = null;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy1() {
    }

    /* 靜态工程方法,建立執行個體 */
    public static SingletonLazy1 getInstance() {
        if (instance == null) {
            instance = new SingletonLazy1();
        }
        return instance;
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return instance;
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

test調用對象執行個體中的方法

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy1;

import cn.zygxsq.design.module.singletonPattern.hungryMode.Singleton;

/**
 * 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonTestLazy1 {
    public static void main(String[] args) {
        //不合法的構造函數
        //編譯時錯誤:構造函數 SingletonLazy1() 是不可見的
        //SingletonLazy1 instance = new SingletonLazy1();

        //擷取唯一可用的對象
        SingletonLazy1 instance = SingletonLazy1.getInstance();                                            // 原文:javascript:void(0)

        //調用方法
        instance.dosomething();
    }
}      

執行結果

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 上述懶漢模式可以滿足基本要求,但是,像這樣毫無線程安全保護的類,如果我們把它放入多線程的環境下,肯定就會出現問題了,如何解決?我們首先會想到對getInstance方法加synchronized關鍵字

懶漢模式2,線程安全(不常用)

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy2;

/**
 * 懶漢模式1,線程安全
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy2 {
    /* 持有私有靜态執行個體,防止被引用,此處指派為null,目的是實作延遲加載 */
    private static SingletonLazy2 instance = null;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy2() {
    }

    /*  synchronized加鎖,保證單例 */
    public static synchronized SingletonLazy2 getInstance() {
        if (instance == null) {
            instance = new SingletonLazy2();                                                      // 原文:javascript:void(0)
        }
        return instance;
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return instance;
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

 上述實作方式可以保證線程安全了,但是,synchronized作為修飾符在方法上使用,在性能上會有所下降,因為每次調用getInstance(),都要對對象上鎖,事實上,隻有在第一次建立對象的時候需要加鎖,之後就不需要了,是以,需要改進

懶漢模式3,線程安全,雙重校驗(不常用)

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy3;

/**
 * 懶漢模式1,線程安全
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy3 {
    /* 持有私有靜态執行個體,防止被引用,此處指派為null,目的是實作延遲加載 */
    private static SingletonLazy3 instance = null;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy3() {
    }

    /*  synchronized加鎖,保證單例 */
    public static SingletonLazy3 getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy3.class) {
                if (instance == null) {
                    instance = new SingletonLazy3();
                }
            }
        }
        return instance;
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return instance;
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

 上述實作方式似乎解決了之前提到的問題,将synchronized關鍵字加在了方法内部,也就是說當調用的時候是不需要加鎖的,隻有在instance為null,并建立對象的時候才需要加鎖,性能有一定的提升。但是,這樣的情況,還是有可能有問題的。會出現指令重排序的情況

看下面的情況:在Java指令中建立對象和指派操作是分開進行的,也就是說

instance = new SingletonLazy3(); 這一段代碼

語句并非是一個原子操作,在 JVM 中這句代碼大概做了下面 3 件事情:

1、給 new的對象 配置設定記憶體

2、調用 Singleton 的構造函數來初始化成員變量

3、将引用instance指向配置設定的記憶體空間(執行完這步 instance 就為非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,另外一個線程B搶奪到了CPU的執行權,這時instance已經是非null了(但卻沒有初始化),是以線程B會直接傳回 instance,然後使用,結果就會出現問題了(因為對象還沒有初始化)。

是以對于第三種進行優化的方式,就是對instance加一個volatile可見性 ,防止指令重排序

private volatile static SingletonLazy4 instance = null;

懶漢模式4,線程安全,雙重校驗,volatile可見性,實作較為複雜

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy4;

/**
 * 懶漢模式4,線程安全,雙重校驗,volatile可見性,實作較為複雜
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy4 {
    /* 持有私有靜态執行個體,防止被引用,此處指派為null,目的是實作延遲加載 */
    private volatile static SingletonLazy4 instance = null;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy4() {
    }

    /*  synchronized加鎖,保證單例 */
    public static SingletonLazy4 getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy4.class) {
                if (instance == null) {
                    instance = new SingletonLazy4();                                               // 原文:javascript:void(0)
                }
            }
        }
        return instance;
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return instance;
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

 懶漢模式4基本可以用了,但是實作起來比較繁瑣,還可以有另外一種簡單的方式,使用内部類來實作。

懶漢模式5,線程安全,靜态内部類

使用内部類來維護單例的實作,JVM内部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的(就是加載完畢後别的線程才能使用)。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance隻被建立一次,并且會保證把指派給instance的記憶體初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也隻會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy5;

/**
 * 懶漢模式5,線程安全,靜态内部類
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy5 {
    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy5() {
    }

    /* 此處使用一個内部類來維護單例 */
    private static class SingletonFactory {
        private static SingletonLazy5 instance = new SingletonLazy5();
    }

    /* 擷取執行個體 */
    public static SingletonLazy5 getInstance() {
        return SingletonFactory.instance;                                             // 原文:javascript:void(0)
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return getInstance();
    }

/* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

這種懶漢模式靜态内部類方式和餓漢模式很像,可以和餓漢模式對比着看一下,餓漢模式隻要 Singleton 類被裝載了,那麼 instance 就會被執行個體化(沒有達到懶加載效果),而這種懶漢方式5靜态内部類方式是 SingletonLazy5 類被裝載了,instance 不一定被初始化。因為 SingletonFactory 類沒有被主動使用,隻有通過顯式調用 getInstance 方法時,才會顯式裝載 SingletonFactory 類,進而執行個體化 instance。想象一下,如果執行個體化 instance 很消耗資源,是以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就執行個體化,因為不能確定 Singleton 類還可能在其他的地方被主動使用進而被加載,那麼這個時候執行個體化 instance 顯然是不合适的。這個時候,這種方式相比餓漢模式方式就顯得很合理。

懶漢模式5靜态内部類方式已經很牛了,但是如果在構造函數中抛出異常,執行個體将永遠得不到建立,也會出錯。

還有,懶漢模式5靜态内部類已經很牛了,但是如果遇到反射調用,我們可以使用反射去建立這個類的對象,即使它的構造器是私有的,我們也是可以調用到的,那也可以建立多個執行個體。那麼這個時候我們就需要再次修改代碼去通路别人反射調用構造器。

是以說,十分完美的東西是沒有的,我們隻能根據實際情況,選擇最适合自己應用場景的實作方法。 

反射調用demo

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy5;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * 反射擷取私有構造函數,建立多個執行個體
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonTestLazy5Reflection {
    public static void main(String[] args) throws Exception{

        // 1:通過Class的靜态方法forName加載
        Class aClass = Class.forName("cn.zygxsq.design.module.singletonPattern.lazyMode.lazy5.SingletonLazy5");

        //擷取私有構造方法
        //Constructor con = c.getConstructor(String.class);
        //NoSuchMethodException沒有這個方法異常
        //原因是一開始我們使用的方法隻能擷取公共的,下面這種方式就可以
        Constructor con = aClass.getDeclaredConstructor();

        //用該私有方法建立對象
        //IllegalAccessException:非法通路異常
        //暴力通路
        con.setAccessible(true);//值為true則訓示反射的對象在使用是應該取消Java語言通路檢查

        // 執行個體化對象的方法
        Object o1 = con.newInstance();
        System.out.println(o1);
        //Method m = o1.getClass().getDeclaredMethod("dosomething", null);

        //通路方法
        Method m = aClass.getDeclaredMethod("dosomething", null);

       //調用方法
        m.invoke(o1, null);

        // 第二次建立對象
        Class aClass2 = Class.forName("cn.zygxsq.design.module.singletonPattern.lazyMode.lazy5.SingletonLazy5");
        Constructor con2 = aClass2.getDeclaredConstructor();

        //用該私有方法建立對象
        //IllegalAccessException:非法通路異常
        //暴力通路
        con2.setAccessible(true);//值為true則訓示反射的對象在使用是應該取消Java語言通路檢查

        // 執行個體化對象的方法
        Object o2 = con2.newInstance();
        System.out.println(o2);
    }
}      

執行結果 

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 可以看到多個對象是不同的

懶漢模式6,線程安全,靜态内部類,防止反射

我們為了避免别人反射調用,我們修改一下構造器為下面這樣的

private SingletonLazy6() { if(!flag){

flag = true;

}else{

throw new RuntimeException("不能多次建立單例對象");

}

}

 demo

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6;

/**
 * 懶漢模式6,線程安全,靜态内部類,防止反射多次
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy6 {
    private static boolean flag;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy6() {
        if(!flag){
            flag = true;
        }else{
            throw new RuntimeException("不能多次建立單例對象");
        }
    }

    /* 此處使用一個内部類來維護單例 */
    private static class SingletonFactory {
        private static SingletonLazy6 instance = new SingletonLazy6();
    }

    /* 擷取執行個體 */
    public static SingletonLazy6 getInstance() {
        return SingletonFactory.instance;                                                                   // 原文:javascript:void(0)
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return getInstance();
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

test反射多次調用測試

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * 反射擷取私有構造函數,建立多個執行個體
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonTestLazy6Reflection {
    public static void main(String[] args) throws Exception{

        // 1:通過Class的靜态方法forName加載
        Class aClass = Class.forName("cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6.SingletonLazy6");

        //擷取私有構造方法
        //Constructor con = c.getConstructor(String.class);                                                             // 原文:javascript:void(0)
        //NoSuchMethodException沒有這個方法異常
        //原因是一開始我們使用的方法隻能擷取公共的,下面這種方式就可以
        Constructor con = aClass.getDeclaredConstructor();

        //用該私有方法建立對象
        //IllegalAccessException:非法通路異常
        //暴力通路
        con.setAccessible(true);//值為true則訓示反射的對象在使用是應該取消Java語言通路檢查

        // 執行個體化對象的方法
        Object o1 = con.newInstance();
        System.out.println(o1);
        //Method m = o1.getClass().getDeclaredMethod("dosomething", null);

        //通路方法
        Method m = aClass.getDeclaredMethod("dosomething", null);

       //調用方法
        m.invoke(o1, null);

        // 第二次建立對象
        Class aClass2 = Class.forName("cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6.SingletonLazy6");
        Constructor con2 = aClass2.getDeclaredConstructor();

        //用該私有方法建立對象
        //IllegalAccessException:非法通路異常
        //暴力通路
        con2.setAccessible(true);//值為true則訓示反射的對象在使用是應該取消Java語言通路檢查

        // 執行個體化對象的方法
        Object o2 = con2.newInstance();
        System.out.println(o2);

    }
}      

執行結果

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

3、readResolve方法

 懶漢模式6靜态内部類防止反射,反射的問題處理完了之後,這裡還有一個問題,就是如果把單例對象進行序列化然後再反序列化,那麼記憶體中就會出現倆個一樣的單例對象,隻是記憶體位址不同。這種情況我們可以使用readResolve方法來防止。

private Object readResolve(){.....} 

ObjectInputStream 會檢查對象的class是否定義了readResolve方法。如果定義了,将由readResolve方法指定傳回的對象。傳回對象的類型一定要是相容的,否則會抛出ClassCastException 。

序列化測試

實作一下Serializable 

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6.testReadResolve;

import java.io.Serializable;

/**
 * 懶漢模式6,線程安全,靜态内部類,防止反射多次
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonLazy6Serializable implements Serializable {
    private static boolean flag;

    /* 私有構造方法,防止被執行個體化 */
    private SingletonLazy6Serializable() {
        if(!flag){
            flag = true;
        }else{
            throw new RuntimeException("不能多次建立單例對象");
        }
    }

    /* 此處使用一個内部類來維護單例 */
    private static class SingletonFactory {
        private static SingletonLazy6Serializable instance = new SingletonLazy6Serializable();
    }

    /* 擷取執行個體 */
    public static SingletonLazy6Serializable getInstance() {
        return SingletonFactory.instance;                                                              // 原文:javascript:void(0)
    }

    /* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
    private Object readResolve() {
        return getInstance();
    }

    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");
    }
}      

test調用SingletonLazy6Serializable 

package cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6.testReadResolve;

import cn.zygxsq.design.module.singletonPattern.lazyMode.lazy6.SingletonLazy6;

import java.io.*;

/**
 * 反射擷取私有構造函數,建立多個執行個體
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/4.
 */
public class SingletonTestLazy6ReadResolve {
    public static void main(String[] args) throws Exception{

        // 将SingletonLazy6Serializable類的readResolve()方法注釋執行一下和不注釋執行一下
        // 檢視列印結果
        SingletonTestLazy6ReadResolve singletonTestLazy6ReadResolve = new SingletonTestLazy6ReadResolve();
        singletonTestLazy6ReadResolve.copy();
    }

    //測試方式,把單例對象序列化後再反序列化進而獲得一個新的對象 就相當于複制了一個單例對象
    public SingletonLazy6Serializable copy() throws Exception{
        System.out.println(SingletonLazy6Serializable.getInstance());
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(SingletonLazy6Serializable.getInstance());                                                                     // 原文:javascript:void(0)

        InputStream is = new ByteArrayInputStream(os.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(is);
        SingletonLazy6Serializable obj = (SingletonLazy6Serializable) ois.readObject();
        System.out.println(obj);
        return obj;
    }
}      

 将SingletonLazy6Serializable類的readResolve()方法注釋執行一下和不注釋執行一下,分别檢視一下列印結果

注釋readResolve()方法執行結果,傳回的是記憶體位址不同

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 不注釋readResolve()方法執行結果,傳回的記憶體位址相同

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 這就是為什麼我将單例模式都要加一個readResolve()方法了,這個你們在其他部落格基本上是很難見到的,我這裡(​​小小魚兒小小林​​)講得稍微更詳細更清楚一點,其他人估計隻能講到上述的餓漢模式和懶漢模式5靜态内部類,基本上已經很全很厲害了,其實懶漢模式5靜态内部類基本上也夠用了。能解決系統的97%了

我這裡額外加了一個懶漢模式6靜态内部類防止反射,并且講述了必須加一個readResolve()方法,不然會有序列化問題。 

ObjectOutputStream是怎麼校驗readResolve()的

可以看一下readObject()方法

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解
java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 再看一下

Object obj = readObject0(false);
java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 看一下readOrdinaryObject()方法,很重要

case TC_OBJECT:

                    return checkResolve(readOrdinaryObject(unshared));

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }      

看一下下面的截圖

java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解
readOrdinaryObject(boolean unshared)方法中擷取單例類的ObjectStreamClass對象desc,判斷對象是否能執行個體化。可以則進行執行個體化,至此單例類進行了第一次執行個體化,對象名為obj
java設計模式之單例模式|單例模式之餓漢模式、懶漢模式、枚舉方式|最詳細的6種懶漢模式詳解

 第一次執行個體化完成後,通過反射尋找該單例類中的readResolve()方法,沒有則直接傳回obj對象。這就是我們對沒有readResolve()方法的類進行序列化後生成不同對象的原因。

因為我們有定義readResolve()方法,desc通過invokeReadResolve(Object obj)方法調用readResolve()方法擷取單例對象instance,将他指派給rep

概括一下ObjectOutputStream().readObject()的整個大緻流程

1.我們在單例類中定義一個readResolve()方法,用于傳回instance對象。
2.反序列化擷取單例類對象時調用readObject()方法。
3.readObject()方法中調用readObject0()方法。
4.readObject0()方法中調用readOrdinaryObject(boolean unshared)方法。
5.readOrdinaryObject(boolean unshared)方法中擷取單例類的ObjectStreamClass對象desc,判斷對象是否能執行個體化。可以則進行執行個體化,至此單例類進行了第一次執行個體化,對象名為obj。
6.第一次執行個體化完成後,通過反射尋找該單例類中的readResolve()方法,沒有則直接傳回obj對象。這就是我們對沒有readResolve()方法的類進行序列化後生成不同對象的原因。
7.因為我們有定義readResolve()方法,desc通過invokeReadResolve(Object obj)方法調用readResolve()方法擷取單例對象instance,将他指派給rep。
8.rep與obj進行比較,由于obj是反射擷取的對象,當然與rep不等,于是将rep的值instance指派給obj,将obj傳回,傳回對象instance也就保證了單例。
9.簡而言之就是當我們通過反序列化readObject()方法擷取對象時會去尋找readResolve()方法,如果該方法不存在則直接傳回新對象,如果該方法存在則按該方法的内容傳回對象。      

懶漢模式6靜态内部類防止反射寫起來還是有點複雜的,其實還有一種更簡單的方式,那就是用枚舉的方式

4、枚舉方式,線程安全(不常用)

package cn.zygxsq.design.module.singletonPattern.enumMode;

/**
 * 單例模式,枚舉方式
* 原文:javascript:void(0)
 * Created by yjl on 2022/8/5.
 */
public enum  SingletonEnum {
    INSTANCE;


    /* 要操作的一些方法*/
    public void dosomething(){
        System.out.println("單例模式方法調用");                                                                        // 原文:javascript:void(0)
    }
}      

調用的話就跟枚舉方式調用的方式一樣

package cn.zygxsq.design.module.singletonPattern.enumMode;

/**
 * Created by yjl on 2022/8/5.
 */
public class SingletonEnumTest {
    public static void main(String[] args) {
        SingletonEnum instance = SingletonEnum.INSTANCE;
        instance.dosomething();
    }
}      

 這種實作方式還沒有被廣泛采用,但這是實作單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次執行個體化。

這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的對象,絕對防止多次執行個體化。不過,由于 JDK1.5 之後才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。

不能通過 reflection attack 來調用私有構造方法。

三、項目位址

以上代碼在下面位址中有全部代碼

​​https://github.com/jalenFish/design-patterns/​​

繼續閱讀