RE|GoF的23種設計模式-3

單例模式

簡介

  1. 單例模式是一種經常使用的軟件設計模式,其定義是單例對象的類只能容許一個實例存在。java

  2. 許多時候整個系統只須要擁有一個的全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。設計模式

基本的實現思路

a. 單例模式要求類可以有返回對象一個引用(永遠是同一個)和一個得到該實例的方法(必須是靜態方法,一般使用getInstance這個名稱)。緩存

b. 單例的實現主要是經過如下兩個步驟: 將該類的構造方法定義爲私有方法,這樣其餘處的代碼就沒法經過調用該類的構造方法來實例化該類的對象,只有經過該類提供的靜態方法來獲得該類的惟一實例; 在該類內提供一個靜態方法,當咱們調用這個方法時,若是類持有的引用不爲空就返回這個引用,若是類保持的引用爲空就建立該類的實例並將實例的引用賦予該類保持的引用。安全

注意事項

單例模式在多線程的應用場合下必須當心使用。若是當惟一實例還沒有建立時,有兩個線程同時調用建立方法,那麼它們同時沒有檢測到惟一實例的存在,從而同時各自建立了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例惟一的原則。 解決這個問題的辦法是爲指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會下降效率)。服務器

1.餓漢式 (線程安全)

餓漢式單例是在類加載的時候就當即初始化,而且建立單例對象。絕對線程安全,在線 程還沒出現之前就是實例化了,不可能存在訪問安全問題。多線程

優勢:沒有加任何的鎖、執行效率比較高,在用戶體驗上來講,比懶漢式更好。併發

缺點:類加載的時候就初始化,無論用與不用都佔着空間,浪費了內存,有可能佔着茅 坑不拉屎。ide

/** * 靜態變量,在類初始化的時候就建立了這個對象,佔資源 */
private final static HungrySingPrinciple hungrySingPrinciple = new HungrySingPrinciple();

/** * 靜態大代碼塊的寫法 */
// private static HungrySingPrinciple hungrySingPrinciple;
//
// static {
// hungrySingPrinciple = new HungrySingPrinciple();
// }

private HungrySingPrinciple() {}

public static HungrySingPrinciple getInstance() {
    return hungrySingPrinciple;
}

複製代碼

2.懶漢式 (線程不安全)

懶漢式被外部類調用的時候內部類纔會加載,下面看懶漢式單例的簡單測試

優勢:這種寫法起到了 Lazy Loading(延遲加載)的效果,可是隻能在單線程下使用。flex

缺點:若是在多線程下,一個線程進入了if (slackerSingleton == null)判斷語句塊,還將來得及往下執行,另外一個線程也經過了這個判斷語句,這時便會產生多個實例。因此在多線程環境下不可以使用這種方式。

private static LazySingPrinciple lazySingPrinciple;

private LazySingPrinciple() {}

public static LazySingPrinciple getInstance() {
    if (lazySingPrinciple == null) lazySingPrinciple = new LazySingPrinciple();
    return lazySingPrinciple;
}

// 測試

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    //信號量,此處用於控制併發的線程數
    final Semaphore semaphore = new Semaphore(100);
    //閉鎖,可實現計數器遞減
    final CountDownLatch countDownLatch = new CountDownLatch(100);
    for (int i = 0  ; i < 100 ; i++) {
        executorService.execute(() -> {
            try {
                //執行此方法用於獲取執行許可,當總計未釋放的許可數不超過200時,
                //容許通行,不然線程阻塞等待,直到獲取到許可。
                semaphore.acquire();
                System.out.println(LazySingPrinciple.getInstance());
                //釋放許可
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
            //閉鎖減一
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();//線程阻塞,直到閉鎖值爲0時,阻塞才釋放,繼續往下執行
    executorService.shutdown();
}

// 結果
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4c4b666e // 不同
com.reape.design.pattern.sing.LazySingPrinciple@6f1cf112 // 不同
com.reape.design.pattern.sing.LazySingPrinciple@15585f4c // 不同
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5

複製代碼

3.懶漢式【加鎖-同步方法】 (線程安全)

private static SynchronizationMethodLazySingPrinciple synchronizationMethodLazySingPrinciple;

private SynchronizationMethodLazySingPrinciple() {}

// 鎖住了方法----效率低下
public synchronized static SynchronizationMethodLazySingPrinciple getInstance() {
    if (synchronizationMethodLazySingPrinciple == null)
        synchronizationMethodLazySingPrinciple = new SynchronizationMethodLazySingPrinciple();
    return synchronizationMethodLazySingPrinciple;
}


複製代碼

優勢:這種寫法起到了 Lazy Loading(延遲加載)的效果,能夠在多線程下使用。

缺點:效率過低了,每一個線程在想得到類的實例時候,執行getInstance()方法都要進行同步。而其實這個方法只執行一次實例化代碼就夠了,後面的想得到該類實例,直接return就好了。方法進行同步效率過低要改進。

4.懶漢式【加鎖-同步代碼塊】 (線程安全)

private static SynchronizationBlockLazySingPrinciple synchronizationBlockLazySingPrinciple;

private SynchronizationBlockLazySingPrinciple() {}

public static SynchronizationBlockLazySingPrinciple getInstance() {
    // 若是此處爲null 者進入同步代碼塊,若是同時也有其餘方法也進入,同時執行完成了建立了實例
    // 若是咱們不判斷null則會建立多個SynchronizationBlockLazySingPrinciple實例
    if (synchronizationBlockLazySingPrinciple == null) {
        synchronized (SynchronizationBlockLazySingPrinciple.class) {
            if (synchronizationBlockLazySingPrinciple == null)
                synchronizationBlockLazySingPrinciple = new SynchronizationBlockLazySingPrinciple();
        }
    }
    return synchronizationBlockLazySingPrinciple;
}
    
複製代碼

優勢:這種寫法起到了 Lazy Loading(延遲加載)的效果,能夠在多線程下使用,效率較高。

缺點:會被反射暴力破解(下面講解)

5.靜態內部類 (線程安全)

/** * 建立一個私有的靜態內部類 * 靜態內部類是在外面的類實例化後建立的類 */
private static class SingletonInstance {
    private final static InnerClassSingPrinciple instance = new InnerClassSingPrinciple();
}

/** * 私有的外部類不能實例化 */
private InnerClassSingPrinciple() {}

/** * 返回實例化的對象 */
public static InnerClassSingPrinciple getInstance() {
    return SingletonInstance.instance;
}

複製代碼

優勢:這種寫法起到了 Lazy Loading(延遲加載)的效果,能夠在多線程下使用,效率較高。

缺點:會被反射暴力破解(下面講解)

6.註冊式單例[枚舉單例] (線程安全)

註冊式單例又稱爲登記式單例,就是將每個實例都登記到某一個地方,使用惟一的標識獲取實例。註冊式單例有兩種寫法:一種爲容器緩存,一種爲枚舉登記。

public enum  EnumSingPrinciple {

    INSTANCE;

    private EnumSingPrincipleObject object;

    EnumSingPrinciple() {
        this.object = new EnumSingPrincipleObject();
    }

    public EnumSingPrincipleObject getEnumSingPrincipleObject() {
        return object;
    }

}

複製代碼

優勢:這種寫法起到了 Lazy Loading(延遲加載)的效果,能夠在多線程下使用,效率較高, 並且不會被反射暴力破解(下面講解)。

7.ThreadLocal 線程單例 (單線程安全)

ThreadLocal 不能保證其建立的對象是全局惟一,可是能保證在單個線程中是惟一的,天生的線程安全。

public class ThreadLocalSingPrinciple {

    private static final ThreadLocal<ThreadLocalSingPrinciple> threadLocalInstance =
            ThreadLocal.withInitial(ThreadLocalSingPrinciple::new);
    
    private ThreadLocalSingPrinciple(){}

    public static ThreadLocalSingPrinciple getInstance(){
        return threadLocalInstance.get();
    }
}

// 測試

public static void main(String[] args) throws InterruptedException {
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());

    new Thread(){
        @Override
        public void run() {
            super.run();
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
        }
    }.start();
    
}

// 結果
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6


// 特色
咱們知道上面的單例模式爲了達到線程安全的目的,給方法上鎖,以時間換空間。
ThreadLocal將全部的對象所有放在 ThreadLocalMap中,爲每一個線程都提供一個對象,其實是以空間換時間來實現線程間隔離的。
複製代碼

總結

單例模式能夠保證內存裏只有一個實例,減小了內存開銷;能夠避免對資源的多重佔用。單例模式看起來很是簡單,實現起來其實也很是簡單。

單例破解

反射破壞單例

public static void main(String[] args) {
    List<Class<?>> classList = new ArrayList<>(
            Arrays.asList(
                    HungrySingPrinciple.class,
                    LazySingPrinciple.class,
                    SynchronizationBlockLazySingPrinciple.class,
                    SynchronizationMethodLazySingPrinciple.class,
                    InnerClassSingPrinciple.class,
                    EnumSingPrinciple.class));
    classList.forEach(item -> {
        try {
            //經過反射拿到私有的構造方法
            Constructor c = item.getDeclaredConstructor();
            //強制訪問,強吻,不肯意也要吻
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(item.getName() + " : " + (o1 == o2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

// 結果
com.reape.design.pattern.sing.HungrySingPrinciple  : false
com.reape.design.pattern.sing.LazySingPrinciple  : false
com.reape.design.pattern.sing.SynchronizationBlockLazySingPrinciple  : false
com.reape.design.pattern.sing.SynchronizationMethodLazySingPrinciple  : false
com.reape.design.pattern.sing.InnerClassSingPrinciple  : false

java.lang.NoSuchMethodException: com.reape.design.pattern.sing.EnumSingPrinciple.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.reape.design.pattern.sing.ReflexDestructionSing.lambda$main$0(ReflexDestructionSing.java:26)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at com.reape.design.pattern.sing.ReflexDestructionSing.main(ReflexDestructionSing.java:23)

// 總結: 除了枚舉其餘的單例都會被反射破解
// 如何防止單例被反射破解?

// 餓漢式,懶漢式 都須要一個變量來標記當前的類是否被加載,可是以前都是保證惟一的入口。
// 經過反射很容易就出現第二個入口,因此必須使用一個反射訪問不到的屬性來標記.
// 咱們使用內部類,這樣又和內部類單例矛盾。
// 下面介紹內部類防禦反射破解

private InnerClassSingPrinciple() {
    // 這個地方不能去掉判斷-->由於第一次執行的時候 SingletonInstance.instance 爲 null
    if (SingletonInstance.instance != null) throw new RuntimeException("滾 ~~");
}

// 爲何枚舉不會被破壞?
// 由於枚舉自己就是一個單例的類,他在構造方法實現了,跟上述錯很少的判斷類型,來判斷從而拒絕重複建立。

複製代碼

序列化破壞單例

public static void main(String[] args) {
    InnerClassSingPrinciple o1 = InnerClassSingPrinciple.getInstance();
    FileOutputStream fos = null;
    FileInputStream fis = null;
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
        // 寫入文件
        fos = new FileOutputStream("SeriableSingleton");
        oos = new ObjectOutputStream(fos);
        oos.writeObject(o1);
        oos.flush();
        oos.close();
        // 讀取文件
        fis = new FileInputStream("SeriableSingleton");
        ois = new ObjectInputStream(fis);
        InnerClassSingPrinciple o2 = (InnerClassSingPrinciple) ois.readObject();
        ois.close();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);  // false
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (fos != null) fos.close();
            if (fis != null) fis.close();
            if (oos != null) oos.close();
            if (ois != null) ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 結果
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
com.reape.design.pattern.sing.InnerClassSingPrinciple@6d6f6e28
false

// 若是咱們吧InnerClassSingPrinciple類裏面寫入一個readResolve方法,以下
private Object readResolve(){
    return SingletonInstance.instance;
}

// 結果
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
true

// 爲何?
// 1. 咱們去看看 InnerClassSingPrinciple o2 = (InnerClassSingPrinciple) ois.readObject(); 的readObject方法
// readObject --->
// readObject0 --->
// readObject0[方法裏面] 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 {
        // 1.執行這個方法
        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);

    // 2.上面賦值了obj
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod()) // 3.執行了這個方法
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

// 對應上面的 --> 1
boolean isInstantiable() {
    // 是判斷一下構造方法是否爲空,構造方法不爲空就返回 true
    requireInitialized();
    return (cons != null);
}



// 對應上面的 --> 3
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException {
    requireInitialized();
    if (readResolveMethod != null) {
        try {
            // 經過反射調用了 readResolve 方法
            return readResolveMethod.invoke(obj, (Object[]) null);
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getTargetException();
            if (th instanceof ObjectStreamException) {
                throw (ObjectStreamException) th;
            } else {
                throwMiscException(th);
                throw new InternalError(th);  // never reached
            }
        } catch (IllegalAccessException ex) {
            // should not occur, as access checks have been suppressed
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

 
// 雖然,增長 readResolve()方法返回實例,解決了單例被破壞的問題。
// 可是,咱們經過分析源碼以及調試,咱們能夠看到實際上實例化了兩次,只不過新建立的對象沒有被返回而已。
// 動態代理的時候沒有調用 原來的 readResolve 而是調用的爲 重寫的 readResolve
複製代碼
相關文章
相關標籤/搜索