設計模式-單例模式詳解

單例模式

​ 保證一個類在任何狀況下都絕對只有一個實例,而且提供一個全局訪問點java

​ 須要隱藏其全部構造方法git

​ 優勢:github

​ 在內存中只有一個實例,減小了內存開銷json

​ 能夠避免對資源的多重佔用設計模式

​ 設置全局訪問點,嚴格控制訪問緩存

​ 缺點:安全

​ 沒有接口,擴展困難jvm

​ 若是要擴展單例對象,只有修改代碼,沒有別的途徑ide

應用場景

​ ServletContext性能

​ ServletConfig

​ ApplicationContext

​ DBPool

常見的單例模式寫法

餓漢式單例

​ 餓漢式就是在初始化的時候就初始化實例

​ 兩種代碼寫法以下:

public class HungrySingleton {
    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {

    }

    private static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}
public class HungryStaticSingleton {
    private static final HungryStaticSingleton HUNGRY_SINGLETON;

    static {
        HUNGRY_SINGLETON = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {

    }

    private static HungryStaticSingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}

​ 若是沒有使用到這個對象,由於一開始就會初始化實例,這種方式會浪費內存空間

懶漢式單例

​ 懶漢式單例爲了解決上述問題,則是在用戶使用的時候才初始化單例

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判斷保證初只會初始化一次
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();//11行
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式,線程不安全,若是兩個線程同時進入11行,那麼會建立兩個對象,須要以下,給方法加鎖

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public synchronized static LazySimpleSingleton getInstance() {
        //加上空判斷保證初只會初始化一次
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式雖然解決了線程安全問題,可是整個方法都是鎖定的,性能比較差,因此咱們使用方法內加鎖的方式解決提升性能

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判斷保證初只會初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {//11行
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式若是兩個線程同時進入了11行,一個線程a持有鎖,一個線程b等待,當持有鎖的a線程釋放鎖以後到return的時候,第二個線程b進入了11行內部,建立了一個新的對象,那麼這時候建立了兩個線程,對象也並非單例的。因此咱們須要在12行位置增長一個對象判空的操做。

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判斷保證初只會初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                if (lazySimpleSingleton != null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式仍是有風險的,由於CPU執行時候會轉化成JVM指令執行:

​ 1.分配內存給對象

​ 2.初始化對象

​ 3.將初始化好的對象和內存地址創建關聯,賦值

​ 4.用戶初次訪問

​ 這種方式,在cpu中3步和4步有可能進行指令重排序。有可能用戶獲取的對象是空的。那麼咱們可使用volatile關鍵字,做爲內存屏障,保證對象的可見性來保證咱們對象的單一。

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判斷保證初只會初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                if (lazySimpleSingleton != null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

靜態內部類單例

​ 還有一種懶漢式單例,利用靜態內部類在調用的時候等到外部方法調用時才執行,巧妙的利用了內部類的特性,jvm底層邏輯來完美的避免了線程安全問題

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {

    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

​ 這種方式雖然可以完美單例,可是咱們若是使用反射的方式以下所示,則會破壞單例

public class LazyInnerClassTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();
        System.out.println(o1 == o2);
    }
}

​ 怎麼辦呢,咱們須要一種方式控制訪問者的行爲,經過異常的方式去限制使用者的行爲,以下所示

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {
        throw new RuntimeException("不容許構建多個實例");
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

​ 還有一種方式會破壞單例,那就是序列化破壞咱們的單例,以下所示

序列化破壞單例

​ 咱們寫一個序列化的方法來嘗試一下上述寫法是不是知足序列化的。

public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton seriableSingleton = SeriableSingleton.getInstance();
        SeriableSingleton s2;
        FileOutputStream fos = null;
        FileInputStream fis = null;

        try {
            fos = new FileOutputStream("d.o");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(seriableSingleton);
            oos.flush();
            oos.close();
            fis = new FileInputStream("d.o");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (SeriableSingleton) ois.readObject();
            ois.close();
            System.out.println(seriableSingleton);
            System.out.println(s2);
            System.out.println(s2 == seriableSingleton);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

​ 爲何序列化會破壞單例呢,咱們查看ObjectInputStream的源碼

首先,咱們查看ObjectInputStream的readObject方法

image-20190312223741407

查看readObject0方法

image-20190312223843082

查看checkResolve(readOrdinaryObject(unshared)方法能夠看到

image-20190312224134367

​ 紅框內三目運算符內若是desc.isInstantiable()爲真就建立新對象,不爲空就返回空,此時咱們查看desc.isInstantiable()方法

image-20190312224253502

此處cons是image-20190312224334399

若是有構造方法就會返回true,固然咱們一個類必然會有構造方法的,因此這就是爲何序列化會破壞咱們的單例

那麼怎麼辦呢,咱們只須要重寫readResolve方法就好了

public class SeriableSingleton implements Serializable {
    private SeriableSingleton() {
        throw new RuntimeException("不容許構建多個實例");
    }

    public static final SeriableSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final SeriableSingleton LAZY = new SeriableSingleton();
    }

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

爲何重寫這個readResolve 的方法就可以避免序列化破壞單例呢

回到上述readOrdinaryObject方法,能夠看到有一個hasReadResolveMethod方法

image-20190312224955112

點進去

image-20190312225117744

image-20190312225150554

能夠看到 readResolveMethod在此處賦值

image-20190312225316878

也就是咱們若是類當中有此方法則在hasReadResolveMethod當中返回的是true

那麼會進入readOrdinaryObject的以下部分

image-20190312225733618

而且以下所示,調用咱們的readResolve方法獲取對象,來保證咱們對象是單例的

image-20190312225838667

​ 可是重寫readResolve方法,只不過是覆蓋了反序列化出來的對象,可是仍是建立了兩次,發生在JVM層面,相對來講比較安全,以前反序列化出來的對象會被GC回收

註冊式單例

枚舉單例

​ 枚舉式單例屬於註冊式單例,他把每個實例都緩存到統一的容器中,使用惟一標識獲取實例。也是比較推薦的一種寫法,以下所示:

public enum EnumSingleton {
    INSTANCE;
    private Object data;

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

​ 反編譯上述文件,能夠看到

image-20190312231120106

​ 那麼序列化能不能破壞枚舉呢

​ 在ObjectInputStream的readObject方法中有針對枚舉的判斷

image-20190312231247835

image-20190312231548907

上述經過一個類名和枚舉名字值來肯定一個枚舉值。從而枚舉在序列化上是不會破壞單例的。

咱們嘗試使用反射來建立一個枚舉對象

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    EnumSingleton() {

    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Class clazz = EnumSingleton.class;
        try {
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
            c.newInstance("dd", 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

拋出異常

image-20190312232850011

查看Constructor源碼能夠看到

image-20190312232036045

能夠看到jdk層面若是判斷是枚舉會拋出異常,因此枚舉式單例是一種比較推薦的單例的寫法。

容器式單例

這種方式是經過容器的方式來保證咱們對象的單例,常見於Spring的IOC容器

public class ContainerSingleton {
    private ContainerSingleton() {

    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        if (!ioc.containsKey(className)) {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();//12
                ioc.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
        return ioc.get(className);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    Object o = ContainerSingleton.getBean("com.zzjson.singleton.register.ContainerSingleton");
                    System.out.println(o + "");
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        executorService.shutdown();

    }

}

這種方式測試可見

image-20190313210600349

出現了幾回不一樣對象的狀況由於咱們線程在12行可能同時進入,這時候咱們須要加一個同步鎖以下,這樣建立對象纔是只會建立一個的

public class ContainerSingleton {
    private ContainerSingleton() {

    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            }
        }
        return ioc.get(className);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    Object o = ContainerSingleton.getBean("com.zzjson.singleton.register.ContainerSingleton");
                    System.out.println(o + "");
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        executorService.shutdown();

    }

}

ThreadLocal單例

這種方式只可以保證在當前線程內的對象是單一的

public class ThreadLocalSingleton {
    private ThreadLocalSingleton() {
    }

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }

    };

    private static ThreadLocalSingleton getInstance() {
        return threadLocalInstance.get();
    }
   }

文中源碼地址設計模式

相關文章
相關標籤/搜索