深度解析單例模式

2023-03-14 18:00:21

飢漢模式

package com.cz.single;

/**
 * @author 卓亦葦
 * @version 1.0
 * 2023/3/11 21:31
 */
public class Hungry {

    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];
    private byte[] data4 = new byte[1024];
    private Hungry(){

    }

    private final static Hungry hungry = new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }

    public static void main(String[] args) {
     .   Hungry.getInstance();
    }
}

會浪費記憶體,執行程式碼,其4個物件已經被建立,浪費空間。

懶漢式單例

package com.cz.single;

/**
 * @author 卓亦葦
 * @version 1.0
 * 2023/3/11 21:35
 */
public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"  OK");
    }
    private volatile static LazyMan lazyMan;

    public static LazyMan getLazyMan(){
        if ((lazyMan==null)){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getLazyMan();
            }).start();
        }
    }
}

懶漢式為使用該物件才建立新物件,但是初始程式碼有問題,單執行緒初始沒有問題,多執行緒會造成,非單例。

解決辦法,首先加鎖,先判斷物件是否為空,如果為空則將class物件進行上鎖,然後需再判斷,鎖是否為空,如果為空再建立新物件。

同步程式碼塊簡單來說就是將一段程式碼用一把鎖給鎖起來, 只有獲得了這把鎖的執行緒才存取, 並且同一時刻, 只有一個執行緒能持有這把鎖, 這樣就保證了同一時刻只有一個執行緒能執行被鎖住的程式碼。第二層,是因為使用同步程式碼塊才加上的,有的可能過了第一個if,沒到同步程式碼塊

為雙層檢測的懶漢式單例,也稱DCL懶漢式

第二個問題

lazyMan = new LazyMan();

程式碼為非原子性操作

建立新物件的底層操作分為3步

1.分配記憶體空間
2、執行構造方法,初始化物件
3、把這個物件指向這個空間
但如果不是原子操作,那132的狀況式可能發現的,如果在A還沒完成構造是,執行緒B進來,則不會執行if語句,發生錯誤

讓lazyMan加上volatile引數

反射會破壞單例模式

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

//        for (int i = 0; i < 10; i++) {
//            new Thread(()->{
//                LazyMan.getLazyMan();
//            }).start();
//        }
        LazyMan lazyMan1 = LazyMan.getLazyMan();
        //獲得空參構造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //反射的setAccessible(true)引數,無視私有構造器
        declaredConstructor.setAccessible(true);
        //通過反射建造物件
        LazyMan lazyMan2 = declaredConstructor.newInstance();

        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
    }

解決辦法,在無參構造器新增異常

private LazyMan(){
    synchronized (LazyMan.class){
        if (lazyMan!=null){
            throw new RuntimeException("不要試圖反射破壞");
        }
    }
    System.out.println(Thread.currentThread().getName()+"  OK");
}

但依然存在問題, if (lazyMan!=null)為非原子性操作,依然存在兩個反射物件導致出現非單例的狀況

//可以採用紅綠燈解決,定義一個金鑰
private static boolean key = false;
private LazyMan(){
    synchronized (LazyMan.class){
        if (key==false){
            key=true;
        }else {

                throw new RuntimeException("不要試圖反射破壞");
             
        }
    }
    System.out.println(Thread.currentThread().getName()+"  OK");
}

但是如果仍然用反射破環,假設獲取到了金鑰的情況

//通過反射修改靜態引數
Field key1 = LazyMan.class.getDeclaredField("key");
key1.setAccessible(true);

//獲得空參構造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//反射的setAccessible(true)引數,無視私有構造器
declaredConstructor.setAccessible(true);
//通過反射建造物件
LazyMan lazyMan1 = declaredConstructor.newInstance();

//修改物件的金鑰引數
key1.set(lazyMan1,false);

LazyMan lazyMan2 = declaredConstructor.newInstance();

System.out.println(lazyMan1);
System.out.println(lazyMan2);

單例仍然會被破壞

真實有效的方式列舉

package com.cz.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author 卓亦葦
 * @version 1.0
 * 2023/3/14 16:26
 */
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        //EnumSingle instance2 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

至此才得以真正解決