設(shè)計(jì)模式初探-創(chuàng)建型模式之單例模式

上一篇文章從NIO原理到性能優(yōu)化中获列,我 們引入了性能調(diào)優(yōu)的相關(guān)話題,著重了談了一下互聯(lián)網(wǎng)架構(gòu)的演變之路蛔垢。這篇文章击孩,我們將會(huì)開(kāi)啟一個(gè)老生常談的新專(zhuān)題-設(shè)計(jì)模式。
提劍跨騎揮鬼雨鹏漆,白骨如山鳥(niǎo)驚飛巩梢。塵世如潮人如水,只嘆江湖幾人回艺玲!
如果將代碼比作江湖括蝠,那么語(yǔ)法就是一招一式,而設(shè)計(jì)模式則是神秘莫測(cè)的內(nèi)功心法饭聚,將其融會(huì)貫通之日忌警,便是立于不敗之時(shí)。本文分為如下部分:

  1. 前言
  2. 設(shè)計(jì)模式分類(lèi)
  3. 單例
    3.1 使用場(chǎng)景
    3.2 單例的5種實(shí)現(xiàn)
    3.3 餓漢單例
    3.3.1 破壞單例
    3.3.2 序例化和反序例化的實(shí)現(xiàn)機(jī)制
    3.3.3 通過(guò)readResolve 來(lái)改善單例
    3.4 懶漢單例
    3.4.1 破壞單例
    3.5 雙重鎖單例
    3.5.1 破壞單例
    3.6 內(nèi)部類(lèi)單例
    3.6.1 破壞單例
    3.7 枚舉單例
    3.7.1 破壞單例
    3.8 幾種單例的比對(duì)
    3.9 基于單例編寫(xiě)文件加載
  4. 總結(jié)

1. 前言

在說(shuō)設(shè)計(jì)模式之前我們先思考幾個(gè)問(wèn)題秒梳。

  1. 什么是設(shè)計(jì)模式法绵?
  2. 什么情況下要用到設(shè)計(jì)模式箕速?
    那么帶著這些問(wèn)題,我們推開(kāi)設(shè)計(jì)模式的大門(mén)

2. 設(shè)計(jì)模式分類(lèi)

所謂設(shè)計(jì)模式指的是在某些特定情境下朋譬,針對(duì)某些特定問(wèn)題的某種解決方案盐茎。它不是語(yǔ)法規(guī)定,而是一套解決方案徙赢,使用設(shè)計(jì)模式幫助我們提高代碼的復(fù)用性字柠、可維護(hù)性、健壯性以及安全性犀忱。同時(shí)更方便和清晰的通過(guò)代碼傳遞我們coding時(shí)的設(shè)計(jì)思路募谎,畢竟好的代碼自己就是注釋。設(shè)計(jì)模式按照意圖可以分為以下幾類(lèi):

意圖分類(lèi) 解釋 模式
創(chuàng)建型 創(chuàng)建型模式涉及到對(duì)象的實(shí)例化阴汇,這類(lèi)模式提供一個(gè)方法数冬,將客戶(hù)從對(duì)象的創(chuàng)建種解耦 工廠方法、抽象工廠搀庶、單例拐纱、建造者、原型
行為型 涉及到類(lèi)和對(duì)象如何交互及分配職責(zé) 模板方法哥倔、命令秸架、迭代器、觀察者咆蒿、策略东抹、狀態(tài)、職責(zé)鏈沃测、中介者缭黔、訪問(wèn)者、備忘者蒂破、解釋器
結(jié)構(gòu)型 結(jié)構(gòu)型模式可以讓你把類(lèi)或?qū)ο蠼M合到更大的結(jié)構(gòu)中去 適配器馏谨、裝飾器、代理附迷、組合惧互、門(mén)面、橋接喇伯、享元

值得注意的是:

  1. 設(shè)計(jì)模式職期間并不是毫無(wú)關(guān)聯(lián)的喊儡,常常一種模式里會(huì)包含另一種模式(如用工工廠方法實(shí)現(xiàn)抽象工廠)
  2. 設(shè)計(jì)模式并不是一塵不變的稻据,常常為了實(shí)現(xiàn)需求而做不同的改變(比如spring的bean是單例管宵,但卻不是我們常用的單例模式,而是注冊(cè)表單例)攀甚。
  3. 不要濫用設(shè)計(jì)模式箩朴,采用最簡(jiǎn)單直觀的方式解決問(wèn)題才是最好的選擇,不要過(guò)分設(shè)計(jì)秋度。

3. 單例

所謂單例即確保一個(gè)類(lèi)只有一個(gè)實(shí)例并提供一個(gè)全局的訪問(wèn)點(diǎn)炸庞。

3.1 使用場(chǎng)景

從單例模式的定義中我們就可以看出單例的使用場(chǎng)景,即只需要一個(gè)對(duì)象的時(shí)候荚斯。那么什么時(shí)候只需要一個(gè)對(duì)象呢埠居?即當(dāng)對(duì)象的創(chuàng)建和銷(xiāo)毀代價(jià)比較高的時(shí)候。如配置文件的加載事期,如spring bean的創(chuàng)建滥壕。

3.2 單例模式的5種實(shí)現(xiàn)方式

單例模式一般有如下實(shí)現(xiàn)方式

  • 餓漢
  • 懶漢
  • 雙重鎖
  • 內(nèi)部類(lèi)
  • 枚舉

3.3 餓漢單例

所謂餓漢,即類(lèi)初始化的時(shí)候就實(shí)例化一個(gè)不可變的靜態(tài)對(duì)象兽泣。代碼如下

public class Singleton implements Serializable {
    private static final Singleton INSTANCE=new Singleton();
    private Singleton(){

    }

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

3.3.1 破壞單例

上面的單例看起來(lái)有點(diǎn)薄弱绎橘,那么它能實(shí)現(xiàn)我們要的單利效果嗎?讓我們來(lái)測(cè)一下唠倦,畢竟不想當(dāng)測(cè)試的程序員不是一個(gè)好UI称鳞。

  • 單線程模式
    /**
     * 單線程調(diào)用
     */
    public void testWithOneThread() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Singleton.getInstance());
        }
    }
   public static void main(String[] args) {
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithOneThread();
    }

輸出結(jié)果如下:

com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@610455d6

看起來(lái)運(yùn)行的很完美,但是這樣就完了嗎稠鼻?


我們嘗試從以下幾方面嘗試來(lái)打破單例
1. 多線程
2. 反射
3. 序列化和反序列化
ps:由于我們的類(lèi)未考慮要實(shí)現(xiàn)Cloneable接口冈止,故clone 的單利破壞不在討論范圍之內(nèi)

  • 多線程
 /**
     * 多線程調(diào)用
     */
    public void testWithMultiThread() {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                System.out.println(Singleton.getInstance());
            });
        }
    }
 public static void main(String[] args) {
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithMultiThread();
    }

運(yùn)行結(jié)果如下:

com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054
com.designPatterns.singleton.eager.Singleton@68674054

我們可以看到餓漢模式實(shí)現(xiàn)的單利,在多線程情況下運(yùn)行良好候齿,static 對(duì)象只在初始化的時(shí)候被加載一次熙暴,這點(diǎn)是由JVM保證的

  • 反射
 /**
     * 反射調(diào)用
     * @throws Exception
     */
    public void testWithReflect() throws Exception{
        Constructor[] constructors = Singleton.class.getDeclaredConstructors();
        Constructor defaultConstructor = constructors[0];
        defaultConstructor.setAccessible(true);
        Singleton    singleton = (Singleton) defaultConstructor.newInstance(null);
        System.out.println(Singleton.getInstance());
        System.out.println(singleton);
    }

    public static void main(String[] args) throws Exception{
        SingletonTest singletonTest=new SingletonTest();
        singletonTest.testWithReflect();
    }
com.designPatterns.singleton.eager.Singleton@610455d6
com.designPatterns.singleton.eager.Singleton@511d50c0

我們可以看到,通過(guò)反射調(diào)用構(gòu)造方法慌盯,可以得到兩個(gè)不同的對(duì)象周霉,餓漢單利在反射情況下不能良好運(yùn)行

  • 序列化和反序列化
  /**
     * 序列化反序列化調(diào)用
     *
     * @throws Exception
     */
    public void testWithStream() throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("\\singleton"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("\\singleton")));) {
            oos.writeObject(Singleton.getInstance());
            Singleton singleton = (Singleton) ois.readObject();
            System.out.println(Singleton.getInstance());
            System.out.println(singleton);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Exception {
        SingletonTest singletonTest = new SingletonTest();
        singletonTest.testWithStream();
    }
com.designPatterns.singleton.eager.Singleton@266474c2
com.designPatterns.singleton.eager.Singleton@2f4d3709

我們看到通過(guò)序列化和反序列化,我們也成功的打破了餓漢單利润匙。

通過(guò)上面的實(shí)例诗眨,我們看到,餓漢模式在單線程及多線程環(huán)境下可以良好運(yùn)行孕讳。在反射匠楚、序列化和反序列化的情況下無(wú)法良好運(yùn)行。

3.3.2 序列化和反序列化的實(shí)現(xiàn)機(jī)制

當(dāng)我們將一個(gè)對(duì)象在磁盤(pán)上存儲(chǔ)厂财,或者網(wǎng)上傳輸?shù)臅r(shí)候芋簿,需要將其轉(zhuǎn)換成byte[],這個(gè)過(guò)程稱(chēng)之為序列化璃饱,其相反的過(guò)程為反序列化与斤。實(shí)現(xiàn)序列化的方式有兩種,實(shí)現(xiàn)Serializable\Externalizable 接口,下面為其異同

類(lèi)目 是否要public constructor 實(shí)例化的方式 自定義邏輯的方式
Serializable no 查找最近父類(lèi)撩穿,調(diào)用父類(lèi)的無(wú)參構(gòu)造器來(lái)實(shí)例化磷支,逐個(gè)屬性賦值 定義私有readObject\writeObject\readResolve
Externalizable yes 調(diào)用當(dāng)前類(lèi)的無(wú)參public constructor 實(shí)例化,調(diào)用readExternal為屬性賦 重寫(xiě)writeExternal/readExternal 方法

下面以Serializable為例來(lái)做個(gè)示范

public class Student implements Serializable{
    private String name;
    private int age;
    private transient String addr;

    private Student(){
        System.out.println("構(gòu)造方法被調(diào)用了");
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }


    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", addr='" + addr + '\'' +
                '}';
    }

    /**
     *  定制序列化
     * @param oos
     * @throws Exception
     */
    private void writeObject(ObjectOutputStream oos)throws Exception{
        System.out.println("writeObject 調(diào)用了");
        oos.defaultWriteObject();
        oos.writeObject((addr+"test"));
    }

    /**
     * 定制反序列化
     * @param ois
     * @throws Exception
     */
    private void readObject(ObjectInputStream ois)throws Exception{
        System.out.println("readObject 調(diào)用了");
        ois.defaultReadObject();
        addr=(String) ois.readObject();

    }

    /**
     * 替換readObject后的值
     * @return
     */
    private Object readResolve(){
        System.out.println("readResolve 調(diào)用了");
        return this;
    }
    private static Student initStudent(){
        Student student=new Student();
        student.setAddr("上海市");
        student.setAge(20);
        student.setName("zhangsan");
        System.out.println("init student:"+student);
        return student;
    }

    public static void main(String[] args) throws Exception{
        Student student=initStudent();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("\\sitx"));
        //將對(duì)象轉(zhuǎn)為byte[] 輸出
        oos.writeObject(student);
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(new File("\\sitx")));
        //從byte[] 中讀取對(duì)象
        Student std= (Student)ois.readObject();
        System.out.println("readStudent:"+std);
    }
}

輸出結(jié)果:

構(gòu)造方法被調(diào)用了
init student:Student{name='zhangsan', age=20, addr='上海市'}
writeObject 調(diào)用了
readObject 調(diào)用了
readResolve 調(diào)用了
readStudent:Student{name='zhangsan', age=20, addr='上海市test'}

我們可以看到readObject 實(shí)例化的時(shí)候食寡,并沒(méi)有調(diào)用我們私有的構(gòu)造器雾狈。同時(shí)readObject的調(diào)用順序要先于readResolve。同時(shí)我們發(fā)現(xiàn)了一個(gè)有趣的現(xiàn)象抵皱,Student類(lèi)中的三個(gè)方法readObject\writeObject\readResolve均為私有方法善榛,是怎么被外部調(diào)用的呢?

疑問(wèn).jpg

原來(lái)ObjectOutputStream.writeObject和ObjectInputStream.readObject的過(guò)程中呻畸,會(huì)通過(guò)先按照默認(rèn)的方式序列化/反序列化所有的非transient屬性移盆,然后再利用反射調(diào)用類(lèi)中的私有writeObject/readObject/readReSolve方法
源碼如下:

  • 在ObjectStreamClass實(shí)例化的時(shí)候伤为,通過(guò)反射查找并初始化上面的幾個(gè)私有方法
if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (isEnum) {
                        suid = Long.valueOf(0);
                        fields = NO_FIELDS;
                        return null;
                    }
                    if (cl.isArray()) {
                        fields = NO_FIELDS;
                        return null;
                    }

                    suid = getDeclaredSUID(cl);
                    try {
                        fields = getSerialFields(cl);
                        computeFieldOffsets();
                    } catch (InvalidClassException e) {
                        serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                        fields = NO_FIELDS;
                    }

                    if (externalizable) {
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    domains = getProtectionDomains(cons, cl);
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                    return null;
                }
            });
        }
  • 在ObjectOutputStream 中 invokeMethod
 private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);
            }
        }
    }
void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (writeObjectMethod != null) {
            try {
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

3.3.3 通過(guò)readResolve 來(lái)改善單例

通過(guò)上面的介紹咒循,大致可以了解readResolve的用法。下面我們用其來(lái)完善餓漢單例钮呀。

public class Singleton implements Serializable {
    private static final Singleton INSTANCE=new Singleton();
    private Singleton(){

    }

    public static Singleton getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }
}
  • 序列化和反序列化測(cè)試
   /**
     * 序列化反序列化調(diào)用
     *
     * @throws Exception
     */
    public void testWithStream() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("\\singleton"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("\\singleton")));) {
            oos.writeObject(Singleton.getInstance());
            Singleton singleton = (Singleton) ois.readObject();
            System.out.println(Singleton.getInstance());
            System.out.println(singleton);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Exception {
        SingletonTest singletonTest = new SingletonTest();
        singletonTest.testWithStream();
    }

結(jié)果如下:

com.designPatterns.singleton.eager.Singleton@b4c966a
com.designPatterns.singleton.eager.Singleton@b4c966a

成功的防止了在序列化和反序列化情況下單利破壞剑鞍。


3.4 懶漢單例

懶漢單利誓琼,即用到的時(shí)候才實(shí)例化的單例人断。這里的懶指的是懶加載。

public class Singleton implements Serializable {
    private static Singleton INSTANCE = null;

    private Singleton() {

    }

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

}

3.4.1 破壞單例

我們分別在單線程環(huán)境搀暑、多線程環(huán)境蚂四、反射光戈、序列化和反序列化的情況下獲取單例。
由于代碼與3.3.1 中相同遂赠,故只貼出運(yùn)行結(jié)果

  • 單線程
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@610455d6
  • 多線程
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@7c837e63
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@358b5609
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@49fe0fd2
com.designPatterns.singleton.lazy.Singleton@5bbb3bd2
  • 反射
com.designPatterns.singleton.lazy.Singleton@610455d6
com.designPatterns.singleton.lazy.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.lazy.Singleton@b4c966a
com.designPatterns.singleton.lazy.Singleton@2f4d3709

可以看到久妆,懶漢模式只有在單線程環(huán)境下能良好運(yùn)行。在多線程跷睦、反射筷弦、序列化和反序列化環(huán)境下均無(wú)法保證單例。
當(dāng)然我們同樣可以通過(guò)定義readResolve方法來(lái)讓其在序列化和反序列的情況下正常運(yùn)行抑诸。由于與3.3.3 的代碼完全一致烂琴,此處不做演示。
值得注意的是蜕乡,懶漢模式實(shí)現(xiàn)了懶加載奸绷,這點(diǎn)是餓漢無(wú)法實(shí)現(xiàn)的


3.5 雙重鎖單利

既然餓漢模式實(shí)現(xiàn)了懶加載但是無(wú)法保證線程安全,那么有沒(méi)有一種即能實(shí)現(xiàn)懶加載层玲,又能保證線程安全的單利模式呢号醉?雙重鎖單利(double-check-singleton)就是這種單利反症。
所謂雙重鎖單利,即通過(guò)對(duì)代碼加鎖(可以是語(yǔ)言層面的synchronzied,也可以是jdk層面的 Lock)和雙重非空判斷的方式來(lái)實(shí)現(xiàn)單利畔派。通過(guò)這種方式實(shí)現(xiàn)的單利铅碍,兼具線程安全及懶加載及性能。以下為代碼實(shí)現(xiàn)

   private static volatile Singleton INSTANCE = null;

    private Singleton() {

    }

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

        }
        return INSTANCE;
    }

}

有兩點(diǎn)值得注意的地方

  1. synchronized 加鎖的位置是出于性能的考慮(不加在方法上是為了保證互斥的最小性)
  2. volatile關(guān)鍵字是為了禁止指令的重排序及保證可見(jiàn)性(防止多線程環(huán)境下父虑,拿到已分配內(nèi)存但未初始化的對(duì)象)
    上面的第二點(diǎn)主要是因?yàn)閚ew Singleton() 這個(gè)步驟该酗,是一個(gè)非原子性操作。通過(guò)javap -c Singleton 查看字節(jié)碼:
public static com.designPatterns.singleton.doubleCheck.Singleton getInstance();
    Code:
       0: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
       3: ifnonnull     37
       6: ldc           #3                  // class com/designPatterns/singleton/doubleCheck/Singleton
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      14: ifnonnull     27
      17: new           #3                  // class com/designPatterns/singleton/doubleCheck/Singleton
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
      24: putstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #2                  // Field INSTANCE:Lcom/designPatterns/singleton/doubleCheck/Singleton;
      40: areturn

我們可以看到 instance=new Singleton 分為幾步

  1. 開(kāi)辟對(duì)象空間-new
  2. 調(diào)用構(gòu)造函數(shù)初始化-invokespecial
  3. 為引用賦值-putstatic
    步驟2和3 可能會(huì)發(fā)生指令的重排序士嚎,導(dǎo)致多線程環(huán)境下拿到未初始化的實(shí)例。

3.5.1 破壞單例

我們分別在單線程環(huán)境悔叽、多線程環(huán)境莱衩、反射、序列化和反序列化的情況下獲取單例娇澎。
由于代碼與3.3.1 中相同笨蚁,故只貼出運(yùn)行結(jié)果

  • 單線程
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
  • 多線程
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
com.designPatterns.singleton.doubleCheck.Singleton@3480e44e
  • 反射
com.designPatterns.singleton.doubleCheck.Singleton@610455d6
com.designPatterns.singleton.doubleCheck.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.doubleCheck.Singleton@b4c966a
com.designPatterns.singleton.doubleCheck.Singleton@2f4d3709

可以看到,雙重鎖單例在單線程及多線程環(huán)境下均能很好的工作趟庄,但是在反射及序列化反序列化的情況下均無(wú)法正常工作括细。

3.6 內(nèi)部類(lèi)單例

所謂內(nèi)部類(lèi)單例,即通過(guò)內(nèi)部類(lèi)的方式來(lái)提供懶加載的單例戚啥。

public class Singleton {
    private Singleton() {

    }

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

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

}

值得注意的是:

  1. 實(shí)現(xiàn)了懶加載的單例
  2. 不用加鎖
  3. 外部類(lèi)可以訪問(wèn)內(nèi)部類(lèi)的私有變量(TODO)

3.6.1 破壞單例

我們分別在單線程環(huán)境奋单、多線程環(huán)境、反射猫十、序列化和反序列化的情況下獲取單例览濒。
由于代碼與3.3.1 中相同,故只貼出運(yùn)行結(jié)果

  • 單線程
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@610455d6
  • 多線程
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
com.designPatterns.singleton.holder.Singleton@3480e44e
  • 反射
com.designPatterns.singleton.holder.Singleton@610455d6
com.designPatterns.singleton.holder.Singleton@511d50c0
  • 序列化和反序列化
com.designPatterns.singleton.holder.Singleton@b4c966a
com.designPatterns.singleton.holder.Singleton@2f4d3709

我們可以看到內(nèi)部類(lèi)單例在單線程及多線程環(huán)境下均能很好的運(yùn)行拖云,但是在反射和序列化反序列化的情況下無(wú)法保證單例贷笛。

3.7 枚舉單例

上面的幾種單例模式,不能同時(shí)滿(mǎn)足 懶加載宙项、線程安全乏苦、序列化反序列、反射的情況下單例尤筐。那么就沒(méi)有一種同時(shí)滿(mǎn)足以上幾種情況的單例嗎汇荐?
枚舉單例既能保證線程安全,又能保證序列化和反序列化單例叔磷,也能保證反射單例拢驾。但是無(wú)法懶加載。

public enum Singleton {
    INSTANCE;
    public static Singleton getInstance(){
        return INSTANCE;
    }
}
吃驚.jpg

是不是特別簡(jiǎn)單改基,值得注意的是:

  1. jvm 會(huì)保證enum 構(gòu)造器的私有繁疤,故此處不用寫(xiě)私有構(gòu)造方法

3.7.1 破壞單例

我們分別在單線程環(huán)境咖为、多線程環(huán)境、反射稠腊、序列化和反序列化的情況下獲取單例躁染。
由于代碼與3.3.1 中相同,故只貼出運(yùn)行結(jié)果

  • 單線程
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE/
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
  • 多線程
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
INSTANCE
  • 反射
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.designPatterns.singleton.SingletonTest.testWithReflect(SingletonTest.java:48)
    at com.designPatterns.singleton.SingletonTest.main(SingletonTest.java:74)
  • 序列化和反序列化
INSTANCE
INSTANCE

可以看到架忌,我們無(wú)法通過(guò)反射調(diào)用枚舉類(lèi)型的構(gòu)造方法吞彤。枚舉單利,在單線程叹放、多線程饰恕、反射、序列化反序列化的情況下均能很好的工作井仰。無(wú)需加鎖埋嵌,實(shí)現(xiàn)簡(jiǎn)單,是首推的實(shí)現(xiàn)單例的方式俱恶。

3.8 幾種單例的比對(duì)

單例類(lèi)型 能否懶加載 是否線程安全 是否加鎖 能否通過(guò)反射獲取新的實(shí)例 能否通過(guò)序列化和反序列化獲取新的實(shí)例
餓漢模式 非懶加載 線程安全 不加鎖
懶漢模式 懶加載 線程不安全 不加鎖
雙重鎖單例 非懶加載 線程安全 加鎖
內(nèi)部類(lèi)單例 懶加載 線程安全 不加鎖
枚舉單例 非懶加載 線程安全 不加鎖

可以看到枚舉單例是我們實(shí)現(xiàn)起來(lái)最簡(jiǎn)單雹嗦,也是最高效的單例,不用去考慮私有構(gòu)造器合是,不用去考慮定制反序列化readResolve方法了罪。

3.9 基于單例編寫(xiě)文件加載

假設(shè)有個(gè)需求需要加載資源文件,這種資源文件的加載是比較耗時(shí)的聪全,我們肯定只希望加載一次泊藕,嘗試用枚舉單例解決這個(gè)問(wèn)題。

public enum PropertiesConfig {
    //實(shí)例
    CONFIG;
    //配置文件存儲(chǔ)集合
    private ConcurrentHashMap sources = new ConcurrentHashMap();

    PropertiesConfig() {
        Properties properties = new Properties();
        try {
            properties.load(PropertiesConfig.class.getResourceAsStream("application.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        properties.entrySet().forEach(p -> sources.put(p.getKey(), p.getValue()));
    }

    private static PropertiesConfig getInstance() {
        return CONFIG;
    }

    private Object getPropertiesByKey(String key) {
        return sources.get(key);
    }


    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                System.out.println(PropertiesConfig.getInstance().getPropertiesByKey("url"));
            });
        }
    }
}

4.總結(jié)

本文開(kāi)始荔烧,我們開(kāi)啟了新的話題-設(shè)計(jì)模式吱七。我們先是總結(jié)了設(shè)計(jì)模式的分類(lèi),然后著重剖析了單例模式鹤竭,分析了單例模式的實(shí)現(xiàn)踊餐。but 本文的重點(diǎn)不是單例模式的實(shí)現(xiàn),而是單例模式的破壞臀稚,以及小tips-序列化和反序列自定義吝岭。希望通本文,大家都能對(duì)單例的種種有些許新的感悟吧寺。

由于技術(shù)水平所限窜管,文章難免有不足之處,歡迎大家指出稚机。希望每位讀者都能有新的收獲幕帆,我們下一篇文章-設(shè)計(jì)模式初探-創(chuàng)建型模式之工廠模式 見(jiàn)
.....

參考文章

  • head first 設(shè)計(jì)模式
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赖条,隨后出現(xiàn)的幾起案子失乾,更是在濱河造成了極大的恐慌常熙,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碱茁,死亡現(xiàn)場(chǎng)離奇詭異裸卫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)纽竣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)墓贿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蜓氨,你說(shuō)我怎么就攤上這事聋袋。” “怎么了语盈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵舱馅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我刀荒,道長(zhǎng),這世上最難降的妖魔是什么棘钞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任缠借,我火速辦了婚禮,結(jié)果婚禮上宜猜,老公的妹妹穿的比我還像新娘泼返。我一直安慰自己,他們只是感情好姨拥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布绅喉。 她就那樣靜靜地躺著,像睡著了一般叫乌。 火紅的嫁衣襯著肌膚如雪柴罐。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天憨奸,我揣著相機(jī)與錄音革屠,去河邊找鬼。 笑死排宰,一個(gè)胖子當(dāng)著我的面吹牛似芝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播板甘,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼党瓮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了盐类?” 一聲冷哼從身側(cè)響起寞奸,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤呛谜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蝇闭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體呻率,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年呻引,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了礼仗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逻悠,死狀恐怖元践,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情童谒,我是刑警寧澤单旁,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站饥伊,受9級(jí)特大地震影響象浑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜琅豆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一愉豺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茫因,春花似錦蚪拦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至洛巢,卻和暖如春括袒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狼渊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工箱熬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狈邑。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓城须,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親米苹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糕伐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容