天天看點

破壞單例模式的三種方式

單例模式的定義:

單例模式,是一種常用的軟體設計模式。在它的核心結構中隻包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類隻有一個執行個體。即一個類隻有一個對象執行個體。

在java中,單例模式我們常用的有三種(不曉得哪個天殺的說有七種,我懶得去找……)

其實,我們在日常的應用中,會遇到這麼一些問題:單例模式是怎麼被破壞的?單例模式無堅不摧到底好不好?單例模式既然會被破壞有沒有辦法寫一個無法破壞的單例模式?

這一次我們會讨論關于單例模式的破壞以及單例模式的良性破壞和惡性破壞。首先我們了解一下單例模式的三種模式:

懶漢式:

最常用的一種形式,具體的代碼可以參考這個:

public class IdlerSingleton {

    private static IdlerSingleton idlerSingleton;

    /**
     * 
     */
    private IdlerSingleton() {
        // constructed by Velociraptors
    }

    public static synchronized IdlerSingleton getInstance() {
        if (idlerSingleton == null) {
            idlerSingleton = new IdlerSingleton();
        }
        return idlerSingleton;
    }

}
           

餓漢式:

public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    /**
     * 
     */
    private HungrySingleton() {
        // constructed by Velociraptors
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

}
           

雙重檢鎖式:

public class DoubleLockSingleton {

    private static volatile DoubleLockSingleton doubleLockSingleton;

    /**
     * 
     */
    private DoubleLockSingleton() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingleton getInstance() {
        if (doubleLockSingleton == null) {
            synchronized(DoubleLockSingleton.class) {
                if(doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingleton();
                }
            }
        }
        return doubleLockSingleton;
    }

}
           

乍一看這個雙重鎖與懶漢模式沒什麼差別,但是其實雙重鎖的執行效率更高,而且并發效果更好。

這個模式将同步内容下放到if内部,提高了執行的效率,不必每次擷取對象時都進行同步,隻有第一次才同步,建立了以後就沒必要了。

雙重鎖模式中雙重判斷加同步的方式,比第一個例子中的效率大大提升,因為如果單層if判斷,在伺服器允許的情況下,假設有一百個線程,耗費的時間為100*(同步判斷時間+if判斷時間),而如果雙重if判斷,100的線程可以同時if判斷,理論消耗的時間隻有一個if判斷的時間。是以如果面對高并發的情況,而且采用的是懶漢模式,最好的選擇就是雙重判斷加同步的方式。

以上三個方式執行效果都是使對象隻有一個執行個體,然而在實際的業務需求中,并不是所有人都按照你所制定的規則玩遊戲(就連吃雞都有人開挂呢……)。

那麼到底有什麼情況可以破壞單例模式呢?

1.克隆(clone)

克隆破壞單例模式嚴格來說,并不算是破壞,打比方(注:此處比方不是人)說單例模式其實就是把類鎖住,然後使用這個類的時候每次都是用的相同的一個執行個體,但是這樣就失去了我們對于這個類的掌控,我們也許在往後的日子裡偶爾會有幾天,需要有多個這樣子的執行個體,于是如果把這個類鎖死了,那就沒有意義了。是以我們需要做到平時鎖死,但是關鍵時刻我們能夠撬開,克隆就是這麼一種良性的單例模式破壞方法,具體做法如下:

public class DoubleLockSingleton implements Cloneable {

    private static volatile DoubleLockSingleton doubleLockSingleton;

    /**
     * 
     */
    public DoubleLockSingleton() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingleton getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingleton.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingleton();
                }
            }
        }
        return doubleLockSingleton;
    }

    // Override by Velociraptors
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // auto created by Velociraptors
        return super.clone();
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }

}
           

測試:

public class SingletonLauncher {

    /**
     * @param args
     * @throws CloneNotSupportedException 
     */
    public static void main(String[] args) throws CloneNotSupportedException {
        // auto created by Velociraptors
        DoubleLockSingleton doubleLockSingleton = DoubleLockSingleton.getInstance();
        DoubleLockSingleton doubleLockSingleton2 = (DoubleLockSingleton)doubleLockSingleton.clone();
        if(doubleLockSingleton == doubleLockSingleton2) {
            System.out.println("Singleton break failed");
        } else {
            doubleLockSingleton.method();
            doubleLockSingleton2.method();
        }
    }

}
//Hello DoubleLockSingleton!
//Hello DoubleLockSingleton!
           

結果如下:

Hello DoubleLockSingleton! 
Hello DoubleLockSingleton!
           

正如代碼所示,克隆破壞單例模式需要繼承Colneable接口并且重寫克隆方法,當你的代碼這麼寫的時候意味着你為了以後破壞這個單例模式做出了準備。

2.反射(reflect)

衆所周知,通過java的反射機制,何止是建立一個執行個體,就連映射整個java類本身的結構都易如反掌。

測試:

public class SingletonLauncher {

    /**
     * @param args
     * @throws CloneNotSupportedException
     */
    public static void main(String[] args) throws Throwable {
        // auto created by Velociraptors
        IdlerSingleton idlerSingleton = IdlerSingleton.getInstance();
        Class<?> clazz = Class.forName("com.singleton.d1213.IdlerSingleton");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        IdlerSingleton idlerSingleton2 = (IdlerSingleton) constructor.newInstance();
        if(idlerSingleton == idlerSingleton2) {
            System.out.println("Singleton break failed!");
        }else {
            System.out.println("Singleton break succeed!");
        }
    }

}
// Singleton break succeed!
           

執行結果如下:

Singleton break succeed!
           

這種破壞方法通過setAccessible(true)的方法是java跳過檢測文法,這是不合理的,是一種惡意破壞單例此時我們的做法是:

public class IdlerSingleton {

    private static IdlerSingleton idlerSingleton;

    /**
     * 
     */
    private IdlerSingleton() {
        // constructed by Velociraptors
        synchronized (IdlerSingleton.class) {
            if (ConstantClass.idlerSingletonCount > 0) {
                // 抛出異常,使得構造方法調用中止,破壞行動被遏制
                throw new RuntimeException("This singleton pattern class can not create more object!");
            }
            ConstantClass.idlerSingletonCount++;
        }
    }

    public static synchronized IdlerSingleton getInstance() {
        if (idlerSingleton == null) {
            idlerSingleton = new IdlerSingleton();
        }
        return idlerSingleton;
    }

    /**
     * 
     * @作者 Velociraptors
     * @建立時間 下午4:35:35
     * @版本時間 2017年12月13日
     *
     */
    static class ConstantClass {

        public static int idlerSingletonCount = 0;

        /**
         * 
         */
        private ConstantClass() {
            // constructed by Velociraptors
        }

    }

}
           

測試結果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at com.singleton.d1213.SingletonLauncher.main(SingletonLauncher.java:28)
Caused by: java.lang.RuntimeException: This singleton pattern class can not create more object!
    at com.singleton.d1213.IdlerSingleton.(IdlerSingleton.java:26)
    ... 5 more
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

成功阻擋了一波破壞。

3.序列化(serializable)

序列化也是破壞單例模式的一大利器。其與克隆性質有些相似,需要類實作序列化接口,相比于克隆,實作序列化在實際操作中更加不可避免,有些類,它就是一定要序列化。

例如下面就有一個序列化的類,并且實作了雙重鎖單例模式:

public class DoubleLockSingletonSerializable implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 972132622841L;
    private static volatile DoubleLockSingletonSerializable doubleLockSingleton;

    /**
     * 
     */
    public DoubleLockSingletonSerializable() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingletonSerializable getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingletonSerializable.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingletonSerializable();
                }
            }
        }
        return doubleLockSingleton;
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }

}
           

測試:

public class SingletonLauncher {

    /**
     * @param args
     * @throws CloneNotSupportedException
     */
    @SuppressWarnings("resource")
    public static void main(String[] args) throws Throwable {
        // auto created by Velociraptors
        DoubleLockSingletonSerializable doubleLockSingletonSerializable = DoubleLockSingletonSerializable.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
        objectOutputStream.writeObject(doubleLockSingletonSerializable);
        File file = new File("tempFile");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        DoubleLockSingletonSerializable doubleLockSingletonSerializable2 = (DoubleLockSingletonSerializable) objectInputStream.readObject();
        if (doubleLockSingletonSerializable == doubleLockSingletonSerializable2) {
            System.out.println("Singleton break failed!");
        } else {
            System.out.println("Singleton break succeed!");
        }
    }

}
           

結果如下:

Singleton break succeed!
           
  • 1

通過對Singleton的序列化與反序列化得到的對象是一個新的對象,這就破壞了Singleton的單例性。

檢視ObjectInputStream的源碼會發現問題:其中一個名為readOrdinaryObject的方法

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");
        }
        //start
        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);
        }
        //end

        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);
        //start
        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);
            }
        }
        //end

        return obj;
    }
           

在我标注的這第一部分start-end可以清晰的看出,序列化底層還是采用了反射來破壞單例。然後在仔細看第二部分start-end,我們可以知道,序列化底層采用反射時會檢查,實作了序列化接口的類是否包含readResolve,如果包含則傳回true,然後會調用readResolve方法。着可好辦了,想要阻止序列化破壞單例模式,就隻需要聲明一個readResolve方法就好了。

public class DoubleLockSingletonSerializable implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 972132622841L;
    private static volatile DoubleLockSingletonSerializable doubleLockSingleton;

    /**
     * 
     */
    public DoubleLockSingletonSerializable() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingletonSerializable getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingletonSerializable.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingletonSerializable();
                }
            }
        }
        return doubleLockSingleton;
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }

    private Object readResolve() {
        return doubleLockSingleton;
    }

}
           

再次測試結果為:

Singleton break failed!
           

至此,我了解的一些内容就先表達到這裡了,即使是簡單的單例模式,如果想要深挖原理,還是有的坑讓我們填的。

繼續閱讀