設計模式:單例模式

    單例模式指的是確保一個類在任何情況下都絕對只有一個實例,並提供一個全局訪問點。單例模式是創建型模式。

1、餓漢模式

    餓漢模式指的是在類加載的時候立即進行初始化,並創建單例對象。

    優點:不需要加鎖,執行效率高,實現簡單;

    缺點:初始化後可能有很長一段時間不會使用,導致內存的浪費。

    適用場景:適用於單例對象較少的情況。

代碼示例:

public class HungrySingleton {
    
    private static final HungrySingleton INSTANCE = new HungrySingleton();
    
    private HungrySingleton() {}
    
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

2、懶漢模式

    懶漢模式指的是在外部類調用的時候纔會加載。

2.1 雙重檢查

    double check指的是,先判斷靜態變量實例是否已經存在,如果存在,則直接返回結果;如果不存在,先加鎖,然後再檢查實例是否存在,如果存在則直接返回結果,如果不存在,再去創建實例。

public class LazySingleton {

    // 防止指令重排
    private static volatile LazySingleton INSTANCE;

    private LazySingleton() {};

    public static LazySingleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }

        synchronized (LazySingleton.class) {
            // double check
            if (INSTANCE != null) {
                return INSTANCE;
            }
            INSTANCE = new LazySingleton();
            return INSTANCE;
        }
    }
}

2.2 防止指令重排

    需要注意的是,上面的實例還給靜態變量INSTANCE加了修飾符volatile,用來防止指令重排導致的,調用方獲取到未完全初始化完成的實例。

    正常情況下,INSTANCE = new LazySingleton();任務分成三步:

  1. 分配對象的內存空間;
  2. 初始化對象;
  3. 設置單例指向剛分配的內存地址。

    但是由於2、3步沒有必然的先後順序,所以會被指令重排優化爲:

  1. 分配對象的內存空間;
  2. 設置單例指向剛分配的內存地址;
  3. 初始化對象。

    如果初始化對象花費的時間較長,就會返回給調用方一個未完全初始化的實例。詳情可以參考volatile原理

3、反射破壞單例

    可以通過反射的方式強制調用單例類的構造器,構造新的實例:

    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();

        // reflect
        LazySingleton lazySingleton1 = reflectLazySingleton();
        
        System.out.println(lazySingleton == lazySingleton1);
    }

    private static LazySingleton reflectLazySingleton() {
        try {
            Class<LazySingleton> lazySingletonClass = LazySingleton.class;
            Constructor<LazySingleton> constructor = lazySingletonClass.getConstructor(null);
            constructor.setAccessible(true);
            return constructor.newInstance();
        } catch (Exception e) {
            return null;
        }
    }

    解決方案:在構造器中校驗靜態變量是否爲空,如果不爲空則打斷構造實例的過程:

    private LazySingleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("不允許創建多個實例");
        }
    }

4、序列化破壞單例

4.1 ObjectInputStream反序列化原理

    ObjectInputStream反序列化實例時,通過readObject()方法最終去調用readOrdinaryObject()方法,部分代碼如下:

        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);
        }

    上述邏輯,主要是判斷該類是否有構造器,如果有則構造新的實例,否則返回空。

    繼續往下看,會發現:

        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);
            }
        }

    代碼中會判斷目標類中是否存在readResolve()方法,如果有的話就調用readResolve()方法獲取實例,並返回該實例。

4.2 避免ObjectInputStream反序列化破壞單例

    根據上述分析可知,可以通過實現readResolve()方法來避免ObjectInputStream反序列化破壞單例:

    public Object readResolve() {
        return getInstance();
    }

4.3 注意實現

    1、實際上,在過程中,還是創建了新的實例,只不過實例沒有被返回,該實例在反序列化後會被回收;

    2、這個只能解決二進制序列化導致的單例被破壞的問題,並不能解決json序列化導致的單例被破壞的問題;

    3、如果使用了第三節中防止反射破壞單例的過程,則會導致該實例無法反序列化。

5、枚舉式單例

5.1 實現

    枚舉式單例模式,指的是將通過枚舉類來實現單例:

public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

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

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

5.2 防止序列化破壞

    ObjectInputStream在反序列化枚舉類時,是通過在readObject()方法中調用readObject0(),然後在readObject0()中調用readEnum()來實現的。readEnum()中的部分代碼:

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

    再去看Enum.valueOf()的源碼:

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

    其中,enumType.enumConstantDirectory()是從class對象中返回的實例參數,是一個Map,所以枚舉類的反序列化是通過反序列化的內容(name)從內存中尋找已經存在的枚舉值,所以不會創建新的實例。

5.3 防止反射破壞單例

    反射通過構造器調用newInstance()方法來構造新的實例,newInstance()方法的源碼如下:

    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

    可以看到,如果類型修飾符是枚舉,會直接打斷並拋出異常。

6、容器式單例

    容器式單例用於單例很多的情況,可以統一管理:

public class ContainerSingleton {

    private ContainerSingleton() {
    }

    private static final ConcurrentHashMap<String, Object> MAP = new ConcurrentHashMap<>();

    public static Object getInstance(String type) {
        if (MAP.containsKey(type)) {
            return MAP.get(type);
        }

        try {
            Object value = Class.forName(type).newInstance();
            if (value == null) {
                return null;
            }
            Object newValue = MAP.putIfAbsent(type, value);
            return newValue == null ? value : newValue;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

    這裏通過判斷ConcurrentHashMap的putIfAbsent的返回值是否爲空,來確定是否有其他線程向MAP中放入了新的實例,避免了加鎖帶來的開銷。當然也有缺點,如果併發級別非常非常高,可能會出現多個線程對同一個key創建實例,如果value的內存開銷很大的話,這時對內存造成一定的壓力。

發佈了95 篇原創文章 · 獲贊 15 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章