保證一個類在任何狀況下都絕對只有一個實例,而且提供一個全局訪問點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方法
查看readObject0方法
查看checkResolve(readOrdinaryObject(unshared)
方法能夠看到
紅框內三目運算符內若是desc.isInstantiable()
爲真就建立新對象,不爲空就返回空,此時咱們查看desc.isInstantiable()
方法
此處cons是
若是有構造方法就會返回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
方法
點進去
能夠看到 readResolveMethod在此處賦值
也就是咱們若是類當中有此方法則在hasReadResolveMethod當中返回的是true
那麼會進入readOrdinaryObject
的以下部分
而且以下所示,調用咱們的readResolve
方法獲取對象,來保證咱們對象是單例的
可是重寫readResolve方法,只不過是覆蓋了反序列化出來的對象,可是仍是建立了兩次,發生在JVM層面,相對來講比較安全,以前反序列化出來的對象會被GC回收
枚舉式單例屬於註冊式單例,他把每個實例都緩存到統一的容器中,使用惟一標識獲取實例。也是比較推薦的一種寫法,以下所示:
public enum EnumSingleton { INSTANCE; private Object data; public static EnumSingleton getInstance() { return INSTANCE; } }
反編譯上述文件,能夠看到
那麼序列化能不能破壞枚舉呢
在ObjectInputStream的readObject方法中有針對枚舉的判斷
上述經過一個類名和枚舉名字值來肯定一個枚舉值。從而枚舉在序列化上是不會破壞單例的。
咱們嘗試使用反射來建立一個枚舉對象
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(); } } }
拋出異常
查看Constructor源碼能夠看到
能夠看到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(); } }
這種方式測試可見
出現了幾回不一樣對象的狀況由於咱們線程在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(); } }
這種方式只可以保證在當前線程內的對象是單一的
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(); } }
文中源碼地址設計模式