設計模式 - 由淺入深分析單例模式

由淺入深分析單例模式

單例模式(Singleton Patten):確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例(Ensure a class has only one instance, and provide a global point of access to it)。程序員

Singleton類稱爲單例類,經過使用private的構造函數確保了在一個應用中只產生一個實例,而且是自行實例化的(在Singleton中本身使用new Singleton())。安全

1.單例入門

餓漢式:在類加載時就建立對象實例,而無論實際是否須要建立。多線程

public class Singleton {

    private static final Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

懶漢式:只有調用getInstance的時候,才實例化對象。ide

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

問題:函數

  • 餓漢式沒能作到延遲加載(lazy loading)。所謂延遲加載就是當真正須要數據的時候,才執行數據加載操做,爲了不一些無謂的性能開銷。但餓漢式的好處是線程安全。
  • 上文中的懶漢式單例在多線程環境下可能會有多個進程同時經過(singleton == null)的條件檢查。這樣就建立出了多個實例,而且極可能形成內存泄露。

2.多線程進階

方案1:源碼分析

在getInstance()方法上加synchronized關鍵字。性能

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

問題:只有在單例初始化的時候咱們才須要保證線程安全,其餘時候方法上的synchronized關鍵字只會下降性能。測試

方案2:flex

只在單例初始化的時候加synchronized關鍵字。優化

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

問題:仍是有可能會有多個進程同時經過(singleton == null)的條件檢查,進而建立多個實例。

方案3:

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

問題:和方案1相似,原本只是想讓new這個操做並行,如今只要是進入getInstance()的線程都得同步,影響性能。

方案4:

雙重檢查加鎖(Double-Check Lock)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                //若是被同步的線程中,有一個線程建立了對象,那麼別的線程就不用再建立了
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

問題:DCL失效。

DCL失效主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在JVM中這句話大概作了下面 3 件事情:

  • 給 singleton 分配內存。
  • 調用 Singleton 的構造函數來初始化成員變量,造成實例。
  • 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)。

可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在3執行完畢、2未執行以前,若是另外一個線程搶佔了鎖,這時 instance 已是非 null 了(但卻沒有初始化),因此該線程會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。

方案5(最終版):

加volatile。

public class Singleton {

    private volatile static Singleton singleton = null;

    private Singleton() {
    }

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

使用 volatile 有兩個做用:

  • 這個變量不會在多個線程中保存複本,而是直接從內存讀取。

  • 這個關鍵字會禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。

3.還能更好嗎

老版《Effective Java》中推薦的方式:

public class Singleton {

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

上面這種方式,仍然使用JVM自己的機制保證了線程安全問題:

  • 因爲 SingletonHolder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它只有在getInstance()被調用時纔會真正建立。
  • 同時讀取實例的時候不會進行同步,沒有性能缺陷。
  • 不依賴 JDK 版本。

枚舉實現:

public enum Singleton {

    INSTANCE;

    public void doSomething() {
    }
}

默認枚舉實例的建立是線程安全的,因此不須要擔憂線程安全的問題。可是在枚舉中的其餘方法的線程安全由程序員本身負責。還有防止上面的經過反射機制調用私用構造器。

這個版本基本上消除了絕大多數的問題,代碼也很是簡單,是新版的《Effective Java》中推薦的模式。

4.其餘問題

序列化攻擊:

單例實現採用方案5,序列化攻擊代碼以下:

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化方式破壞單例   測試
        serializeDestroyMethod();
    }

    private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
        Singleton singleton;
        Singleton singletonNew;

        singleton = Singleton.getInstance();

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(singleton);

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        singletonNew = (Singleton) ois.readObject();

        System.out.println(singleton == singletonNew);
    }
}

打印結果爲:false。在單例類中添加一個方法 readResolve():

public class Singleton implements Serializable {
    private volatile static Singleton singleton = null;

    private Singleton() {
    }

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

    private Object readResolve() {
        return singleton;
    }
}

再執行攻擊代碼,打印結果爲:true。

反序列化攻擊源碼分析:

    //默認狀況下 該方法經過反射建立一個新對象並返回
    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 {
            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);

        //通過上面的代碼,新對象已經被new出來了
        //下面hasReadResolveMethod()這個方法很關鍵。若是該類存在readResolve()方法,就調用該方法返回的實例替換掉新建立的對象。若是不存在就直接把new出來的對象返回出去。
        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);
            }
        }

        return obj;
    }

...
/** * 返回該類是否有readResolve方法 */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }

因此在單例類中添加方法 readResolve(),就能夠防範反序列化攻擊。

反射攻擊:

單例實現依然採用方案5,反射攻擊代碼以下:

public class Main {

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

    private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = Singleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        Singleton singleton = Singleton.getInstance();
        Singleton singletonNew = (Singleton) constructor.newInstance();

        System.out.println(singleton == singletonNew);
    }
}

打印結果爲:false。

使用枚舉實現單例後,執行反射攻擊報錯以下:

緣由是Singleton.class.getDeclaredConstructors()獲取全部構造器,會發現並無咱們所設置的無參構造器,只有一個參數爲(String.class,int.class)構造器。看下Enum源碼就明白,這兩個參數是name和ordinal兩個屬性:

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
        public final int ordinal() {
            return ordinal;
        }
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //餘下省略

枚舉Enum是個抽象類,一旦一個類聲明爲枚舉,實際上就是繼承了Enum,因此就會有(String.class,int.class)的構造器。既然無參構造方法找不到,那咱們就使用父類Enum的構造器,看看是什麼狀況:

public class Main {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        //反射方式破壞單例 測試
        reflexDestroyMethod();
    }

    private static void reflexDestroyMethod() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = SingletonE.class;
        Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);

        SingletonE singleton = SingletonE.INSTANCE;
        SingletonE singletonNew = (SingletonE) constructor.newInstance("test", 1);

        System.out.println(singleton == singletonNew);
    }
}

執行結果以下:

說是不能反射建立枚舉對象,newInstance()方法源碼以下:

    @CallerSensitive
    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;
    }

由上文源碼可知,反射在經過newInstance建立對象時,會檢查該類是否ENUM修飾,若是是則拋出異常,反射失敗。

總結:

單元素的枚舉類型已經成爲實現Singleton的最佳方法 —— 《Effective Java》

相關文章
相關標籤/搜索