001設計模式:單例模式

Bbbrook發表於2020-10-08

單例模式的定義:
單例模式是指確保一個類在任何情況下都絕對只有一個例項,並提供一個全域性訪問點。隱藏其所有的構造方法。

建立模式的常見寫法

1:餓漢模式

2:懶漢模式

3:註冊模式

4:ThreadLocal單例

餓漢模式

餓漢模式單例是單例首次載入時就建立例項

/***
 * 1)私有建構函式
 *
 * 2)靜態私有成員--在類載入時已初始化
 *
 * 3)公開訪問點getInstance-----不需要同步,因為在類載入時已經初始化完畢,

 * 也不需要判斷null,直接返回
 *
 * 缺點:不管用沒用到類 都會給你初始化 浪費型別空間
 */
public class HungrySingleton implements Serializable {



    /**
     * 餓漢模式 直接初始化
     */
private static final HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有化 構造方法 別人呼叫不到
     */
    private HungrySingleton() {}


    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

}

缺點:不管用沒用到例項,都會初始化,浪費型別空間。

破壞單例的方式有序列化單例。程式碼如下:

/**
 * 序列化去建立單例
 *
 * @param args
 */
public static void main(String[] args) {


    HungrySingleton s1 = null;
    HungrySingleton s2 = HungrySingleton.getInstance();
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream("HungrySingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s2);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("HungrySingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (HungrySingleton)ois.readObject();
        ois.close();
        System.out.println("s1:"+s1);
        System.out.println("s2"+s2);
        System.out.print("s1和s2的比較為");
        System.out.println(s1 == s2);
    } catch (Exception e) {
        e.printStackTrace();
    }

}

執行結果為false,這是因為序列化的時候再次初始化了單例。

這時候需要在實體類中加上

原始碼分析:

ois.readObject();

-> 431行 readObject(false)

->readOrdinaryObject(unshared)

->2053行 obj = desc.isInstantiable() ? desc.newInstance() : null;

desc.isInstantiable() -> 1023 return (cons != null); //判斷是否有構造方法如果有返回 true

這裡又再次初始化了一次實體類

為了防止這種時間發生 需要在實體類中新增方法

private Object readResolve(){
    return hungrySingleton;
}

原始碼分析:

ois.readObject();

-> 431行 readObject(false)

->1573行 readOrdinaryObject(unshared)

->2074行

obj != null &&handles.lookupException(passHandle) == null &&

desc.hasReadResolveMethod()

前兩個判斷條件為true 第三個判斷條件 hasReadResolveMethod() 字面意思就是看有沒有 readResolve 這個方法 如果有 繼續往下走

->2091行 handles.setObject(passHandle, obj = rep);

obj :為反序列話時候再次初始化的值

rep:為執行readResolve方法返回的值

原始碼分析可以看出實際上建立了兩次,只不過初始化的那個讓GC處理了

懶漢式單例

被外部類呼叫時才建立例項

/**
 * 懶載入:
 * 1) 被外部類呼叫的時候才會建立例項
 * 2)私有建構函式
 */
public class LazySingleton implements Serializable{

    private static LazySingleton lazySingleton;
    private LazySingleton() {}


    /**
     * 為了執行緒安全和效能 把synchronized 放到方法裡邊
     *
     * @return
     */
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            // 如果為空建立
            synchronized (LazySingleton.class) {
         /**
         * 這個時候在去判空一次
            * 防止當lazySingleton 為空的時候 兩個執行緒 同時進入這裡
            */
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }

}

注意:這裡用到雙重檢查鎖

還有一種懶載入方法:靜態內部類,靜態內部類是最高階的單例。

靜態內部類特性:只有在外部呼叫的時候,才會載入。

public class LazyInnerClassSingleton {


private static final LazyInnerClassSingleton lazyInnerClassSingleton

                                         = new LazyInnerClassSingleton();

    private LazyInnerClassSingleton() {
        if (lazyHolder.LAZY != null) {
            throw new RuntimeException("不允許建立");
        }

    }


    /**
     * @return
     */
    public static LazyInnerClassSingleton getInstance() {
        return lazyHolder.LAZY;
    }


    /**
     * 靜態內部類
     * 靜態內部類 是最高階的 單例模式
     * 巧妙的利用了內部類的特性
     * 只有在外部類呼叫的時候 才會去建立內部類
     */
    private static class lazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

}


}

上邊程式碼雖然私有化了構造方法,但是在反射的時候,jvm不管是不是私有化,他會執行他的初始方法。所以在初始化方法加上報錯,防止被多次建立。

反射測試程式碼如下:

/**
  * 靜態內部類有 可能 被反射攻擊
     * 通過反射去獲取他的 構造方法
     */
    public static void main(String[] args) {

        EnumSingleton instance = EnumSingleton.INSTANCE;
        LazyInnerClassSingleton lazyInnerClassSingleton = LazyInnerClassSingleton.getInstance();

        // 獲取他的構造方法
        Class<?> lazyClass = LazyInnerClassSingleton.class;
        try {
            Constructor c = lazyClass.getDeclaredConstructor(null);
            c.setAccessible(true);
            LazyInnerClassSingleton o = (LazyInnerClassSingleton) c.newInstance();

            // 比較 o和 一開始建立的 不是同一個變數
            System.out.print("o和lazyInnerClassSingleton比較結果為: ");
            System.out.println(o == lazyInnerClassSingleton);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

註冊式單例

將每一個例項都快取到統一的容器中,使用唯一表示獲取例項。

最常見的如,spring ioc容器

public class ContainerSngleton {

    private ContainerSngleton() {
    }

private static Map<String, Object> ioc =

                    new ConcurrentHashMap<String, Object>();

    public static Object getBean(String beanName) {
        if (!ioc.containsKey(beanName)) {
            synchronized (ContainerSngleton.class) {
                if (!ioc.containsKey(beanName)) {
                    return ioc.get(beanName);
                }
                Object obj = null;
                try {
                    obj = Class.forName(beanName).newInstance();
                    ioc.put(beanName, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        return ioc.get(beanName);
    }


}

註冊容器裡還有列舉是單例:如

/**
 * 註冊式單例
 *
 * 列舉式單例  這種是執行緒安全的
 *
 * 因為在 通過工具檢視指令時候會發現
 */
public enum EnumSingleton {
    INSTANCE;
    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    private Object data;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }


}

用jad 反編譯 class 檔案可以看到 如下:

(jad下載連結https://varaneckas.com/jad/jad158g.win.zip)

static 

    {

        INSTANCE = new EnumSingleton("INSTANCE", 0);

        $VALUES = (new EnumSingleton[] {

            INSTANCE

        });

    }

對沒錯,餓漢式。jvm編譯了一個static 靜態方法 把裡邊的例項 初始化到一個陣列裡邊。

反射原始碼分析列舉初始化:

newInstance()方法 416行

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);
這裡如果是列舉類 呼叫反射初始化方法,會報錯。

從jdk層面就為列舉不被序列化和反射破壞來保駕護航

彩蛋

用ThreadLocal去初始化單例

public class ThreadLocalSingleton {


    private ThreadLocalSingleton() {}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

然後用多執行緒去呼叫getInstance()方法,你會發現在同一個執行緒裡邊獲取到的單例是同一個物件,不同執行緒獲取到的單例是不同物件。

原始碼分析:

threadLocalInstance.get()

-> Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null) {

ThreadLocalMap.Entry e = map.getEntry(this);

通過原始碼分析他是執行緒間的安全,在一個執行緒中去獲取他是執行緒安全的,在不同執行緒執行緒間他的,物件是不相等的,也可以叫做偽執行緒安全,通過原始碼 可以發現他是以當前執行緒作為key

單例模式的優點:

在記憶體中只有一個例項,減少了記憶體的開銷.

可以避免堆資源的多重佔用。

設定全域性訪問點,嚴格控制訪問

在這裡插入圖片關注描述
--------------------------------------------------------------關注⬆並回復 gp001 獲取原始碼-------------------------------------------------------------

相關文章