深入探索單例模式,真不簡單
前言
單例模式無論在我們面試,還是日常工作中,都會面對的問題。但很多單例模式的細節(jié),值得我們深入探索一下。
這篇文章透過單例模式,串聯(lián)了多方面基礎知識,非常值得一讀。
1 什么是單例模式?
單例模式是一種非常常用的軟件設計模式,它定義是單例對象的類只能允許一個實例存在。
該類負責創(chuàng)建自己的對象,同時確保只有一個對象被創(chuàng)建。一般常用在工具類的實現(xiàn)或創(chuàng)建對象需要消耗資源的業(yè)務場景。
單例模式的特點:
- 類構(gòu)造器私有
- 持有自己類的引用
- 對外提供獲取實例的靜態(tài)方法
我們先用一個簡單示例了解一下單例模式的用法。
- public class SimpleSingleton {
- //持有自己類的引用
- private static final SimpleSingleton INSTANCE = new SimpleSingleton();
- //私有的構(gòu)造方法
- private SimpleSingleton() {
- }
- //對外提供獲取實例的靜態(tài)方法
- public static SimpleSingleton getInstance() {
- return INSTANCE;
- }
- public static void main(String[] args) {
- System.out.println(SimpleSingleton.getInstance().hashCode());
- System.out.println(SimpleSingleton.getInstance().hashCode());
- }
- }
打印結(jié)果:
- 1639705018
- 1639705018
我們看到兩次獲取SimpleSingleton實例的hashCode是一樣的,說明兩次調(diào)用獲取到的是同一個對象。
可能很多朋友平時工作當中都是這么用的,但我要說這段代碼是有問題的,你會相信嗎?
不信,我們一起往下看。
2 餓漢和懶漢模式
在介紹單例模式的時候,必須要先介紹它的兩種非常著名的實現(xiàn)方式:餓漢模式 和 懶漢模式。
2.1 餓漢模式
實例在初始化的時候就已經(jīng)建好了,不管你有沒有用到,先建好了再說。具體代碼如下:
- public class SimpleSingleton {
- //持有自己類的引用
- private static final SimpleSingleton INSTANCE = new SimpleSingleton();
- //私有的構(gòu)造方法
- private SimpleSingleton() {
- }
- //對外提供獲取實例的靜態(tài)方法
- public static SimpleSingleton getInstance() {
- return INSTANCE;
- }
- }
餓漢模式,其實還有一個變種:
- public class SimpleSingleton {
- //持有自己類的引用
- private static final SimpleSingleton INSTANCE;
- static {
- INSTANCE = new SimpleSingleton();
- }
- //私有的構(gòu)造方法
- private SimpleSingleton() {
- }
- //對外提供獲取實例的靜態(tài)方法
- public static SimpleSingleton getInstance() {
- return INSTANCE;
- }
- }
使用靜態(tài)代碼塊的方式實例化INSTANCE對象。
使用餓漢模式的好處是:沒有線程安全的問題,但帶來的壞處也很明顯。
- private static final SimpleSingleton INSTANCE = new SimpleSingleton();
一開始就實例化對象了,如果實例化過程非常耗時,并且最后這個對象沒有被使用,不是白白造成資源浪費嗎?
還真是啊。
這個時候你也許會想到,不用提前實例化對象,在真正使用的時候再實例化不就可以了?
這就是我接下來要介紹的:懶漢模式。
2.2 懶漢模式
顧名思義就是實例在用到的時候才去創(chuàng)建,“比較懶”,用的時候才去檢查有沒有實例,如果有則返回,沒有則新建。具體代碼如下:
- public class SimpleSingleton2 {
- private static SimpleSingleton2 INSTANCE;
- private SimpleSingleton2() {
- }
- public static SimpleSingleton2 getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new SimpleSingleton2();
- }
- return INSTANCE;
- }
- }
示例中的INSTANCE對象一開始是空的,在調(diào)用getInstance方法才會真正實例化。
嗯,不錯不錯。但這段代碼還是有問題。
2.3 synchronized關(guān)鍵字
上面的代碼有什么問題?
答:假如有多個線程中都調(diào)用了getInstance方法,那么都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時默認值是null。這樣會導致多個線程中同時創(chuàng)建INSTANCE對象,即INSTANCE對象被創(chuàng)建了多次,違背了只創(chuàng)建一個INSTANCE對象的初衷。
那么,要如何改進呢?
答:最簡單的辦法就是使用synchronized關(guān)鍵字。
改進后的代碼如下:
- public class SimpleSingleton3 {
- private static SimpleSingleton3 INSTANCE;
- private SimpleSingleton3() {
- }
- public synchronized static SimpleSingleton3 getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new SimpleSingleton3();
- }
- return INSTANCE;
- }
- public static void main(String[] args) {
- System.out.println(SimpleSingleton3.getInstance().hashCode());
- System.out.println(SimpleSingleton3.getInstance().hashCode());
- }
- }
在getInstance方法上加synchronized關(guān)鍵字,保證在并發(fā)的情況下,只有一個線程能創(chuàng)建INSTANCE對象的實例。
這樣總可以了吧?
答:不好意思,還是有問題。
有什么問題?
答:使用synchronized關(guān)鍵字會消耗getInstance方法的性能,我們應該判斷當INSTANCE為空時才加鎖,如果不為空不應該加鎖,需要直接返回。
這就需要使用下面要說的雙重檢查鎖了。
2.4 餓漢和懶漢模式的區(qū)別
but,在介紹雙重檢查鎖之前,先插播一個朋友們可能比較關(guān)心的話題:餓漢模式 和 懶漢模式 各有什么優(yōu)缺點?
- 餓漢模式:優(yōu)點是沒有線程安全的問題,缺點是浪費內(nèi)存空間。
- 懶漢模式:優(yōu)點是沒有內(nèi)存空間浪費的問題,缺點是如果控制不好,實際上不是單例的。
好了,下面可以安心的看看雙重檢查鎖,是如何保證性能的,同時又保證單例的。
3 雙重檢查鎖
雙重檢查鎖顧名思義會檢查兩次:在加鎖之前檢查一次是否為空,加鎖之后再檢查一次是否為空。
那么,它是如何實現(xiàn)單例的呢?
3.1 如何實現(xiàn)單例?
具體代碼如下:
- public class SimpleSingleton4 {
- private static SimpleSingleton4 INSTANCE;
- private SimpleSingleton4() {
- }
- public static SimpleSingleton4 getInstance() {
- if (INSTANCE == null) {
- synchronized (SimpleSingleton4.class) {
- if (INSTANCE == null) {
- INSTANCE = new SimpleSingleton4();
- }
- }
- }
- return INSTANCE;
- }
- }
在加鎖之前判斷是否為空,可以確保INSTANCE不為空的情況下,不用加鎖,可以直接返回。
為什么在加鎖之后,還需要判斷INSTANCE是否為空呢?
答:是為了防止在多線程并發(fā)的情況下,只會實例化一個對象。
比如:線程a和線程b同時調(diào)用getInstance方法,假如同時判斷INSTANCE都為空,這時會同時進行搶鎖。
假如線程a先搶到鎖,開始執(zhí)行synchronized關(guān)鍵字包含的代碼,此時線程b處于等待狀態(tài)。
線程a創(chuàng)建完新實例了,釋放鎖了,此時線程b拿到鎖,進入synchronized關(guān)鍵字包含的代碼,如果沒有再判斷一次INSTANCE是否為空,則可能會重復創(chuàng)建實例。
所以需要在synchronized前后兩次判斷。
不要以為這樣就完了,還有問題呢?
3.2 volatile關(guān)鍵字
上面的代碼還有啥問題?
- public static SimpleSingleton4 getInstance() {
- if (INSTANCE == null) {//1
- synchronized (SimpleSingleton4.class) {//2
- if (INSTANCE == null) {//3
- INSTANCE = new SimpleSingleton4();//4
- }
- }
- }
- return INSTANCE;//5
- }
getInstance方法的這段代碼,我是按1、2、3、4、5這種順序?qū)懙?,希望也按這個順序執(zhí)行。
但是java虛擬機實際上會做一些優(yōu)化,對一些代碼指令進行重排。重排之后的順序可能就變成了:1、3、2、4、5,這樣在多線程的情況下同樣會創(chuàng)建多次實例。重排之后的代碼可能如下:
- public static SimpleSingleton4 getInstance() {
- if (INSTANCE == null) {//1
- if (INSTANCE == null) {//3
- synchronized (SimpleSingleton4.class) {//2
- INSTANCE = new SimpleSingleton4();//4
- }
- }
- }
- return INSTANCE;//5
- }
原來如此,那有什么辦法可以解決呢?
答:可以在定義INSTANCE是加上volatile關(guān)鍵字。具體代碼如下:
- public class SimpleSingleton7 {
- private volatile static SimpleSingleton7 INSTANCE;
- private SimpleSingleton7() {
- }
- public static SimpleSingleton7 getInstance() {
- if (INSTANCE == null) {
- synchronized (SimpleSingleton7.class) {
- if (INSTANCE == null) {
- INSTANCE = new SimpleSingleton7();
- }
- }
- }
- return INSTANCE;
- }
- }
volatile關(guān)鍵字可以保證多個線程的可見性,但是不能保證原子性。同時它也能禁止指令重排。
雙重檢查鎖的機制既保證了線程安全,又比直接上鎖提高了執(zhí)行效率,還節(jié)省了內(nèi)存空間。
除了上面的單例模式之外,還有沒有其他的單例模式?
4 靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類顧名思義是通過靜態(tài)的內(nèi)部類來實現(xiàn)單例模式的。
那么,它是如何實現(xiàn)單例的呢?
4.1 如何實現(xiàn)單例模式?
具體代碼如下:
- public class SimpleSingleton5 {
- private SimpleSingleton5() {
- }
- public static SimpleSingleton5 getInstance() {
- return Inner.INSTANCE;
- }
- private static class Inner {
- private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
- }
- }
我們看到在SimpleSingleton5類中定義了一個靜態(tài)的內(nèi)部類Inner。在SimpleSingleton5類的getInstance方法中,返回的是內(nèi)部類Inner的實例INSTANCE對象。
只有在程序第一次調(diào)用getInstance方法時,虛擬機才加載Inner并實例化INSTANCE對象。
java內(nèi)部機制保證了,只有一個線程可以獲得對象鎖,其他的線程必須等待,保證對象的唯一性。
4.2 反射漏洞
上面的代碼看似完美,但還是有漏洞。如果其他人使用反射,依然能夠通過類的無參構(gòu)造方式創(chuàng)建對象。例如:
- Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
- try {
- SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
- System.out.println(newInstance == SimpleSingleton5.getInstance());
- } catch (InstantiationException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
上面代碼打印結(jié)果是false。
由此看出,通過反射創(chuàng)建的對象,跟通過getInstance方法獲取的對象,并非同一個對象,也就是說,這個漏洞會導致SimpleSingleton5非單例。
那么,要如何防止這個漏洞呢?
答:這就需要在無參構(gòu)造方式中判斷,如果非空,則拋出異常了。
改造后的代碼如下:
- public class SimpleSingleton5 {
- private SimpleSingleton5() {
- if(Inner.INSTANCE != null) {
- throw new RuntimeException("不能支持重復實例化");
- }
- }
- public static SimpleSingleton5 getInstance() {
- return Inner.INSTANCE;
- }
- private static class Inner {
- private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
- }
- }
- }
如果此時,你認為這種靜態(tài)內(nèi)部類,實現(xiàn)單例模式的方法,已經(jīng)完美了。
那么,我要告訴你的是,你錯了,還有漏洞。。。
4.3 反序列化漏洞
眾所周知,java中的類通過實現(xiàn)Serializable接口,可以實現(xiàn)序列化。
我們可以把類的對象先保存到內(nèi)存,或者某個文件當中。后面在某個時刻,再恢復成原始對象。
具體代碼如下:
- public class SimpleSingleton5 implements Serializable {
- private SimpleSingleton5() {
- if (Inner.INSTANCE != null) {
- throw new RuntimeException("不能支持重復實例化");
- }
- }
- public static SimpleSingleton5 getInstance() {
- return Inner.INSTANCE;
- }
- private static class Inner {
- private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
- }
- private static void writeFile() {
- FileOutputStream fos = null;
- ObjectOutputStream oos = null;
- try {
- SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
- fos = new FileOutputStream(new File("test.txt"));
- oos = new ObjectOutputStream(fos);
- oos.writeObject(simpleSingleton5);
- System.out.println(simpleSingleton5.hashCode());
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (oos != null) {
- try {
- oos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- private static void readFile() {
- FileInputStream fis = null;
- ObjectInputStream ois = null;
- try {
- fis = new FileInputStream(new File("test.txt"));
- ois = new ObjectInputStream(fis);
- SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();
- System.out.println(myObject.hashCode());
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } finally {
- if (ois != null) {
- try {
- ois.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- public static void main(String[] args) {
- writeFile();
- readFile();
- }
- }
運行之后,發(fā)現(xiàn)序列化和反序列化后對象的hashCode不一樣:
- 189568618
- 793589513
說明,反序列化時創(chuàng)建了一個新對象,打破了單例模式對象唯一性的要求。
那么,如何解決這個問題呢?
答:重新readResolve方法。
在上面的實例中,增加如下代碼:
- private Object readResolve() throws ObjectStreamException {
- return Inner.INSTANCE;
- }
運行結(jié)果如下:
- 290658609
- 290658609
我們看到序列化和反序列化實例對象的hashCode相同了。
做法很簡單,只需要在readResolve方法中,每次都返回唯一的Inner.INSTANCE對象即可。
程序在反序列化獲取對象時,會去尋找readResolve()方法。
- 如果該方法不存在,則直接返回新對象。
- 如果該方法存在,則按該方法的內(nèi)容返回對象。
- 如果我們之前沒有實例化單例對象,則會返回null。
好了,到這來終于把坑都踩完了。
還是費了不少勁。
不過,我偷偷告訴你一句,其實還有更簡單的方法,哈哈哈。
納尼。。。
5 枚舉
其實在java中枚舉就是天然的單例,每一個實例只有一個對象,這是java底層內(nèi)部機制保證的。
簡單的用法:
- public enum SimpleSingleton7 {
- INSTANCE;
- public void doSamething() {
- System.out.println("doSamething");
- }
- }
在調(diào)用的地方:
- public class SimpleSingleton7Test {
- public static void main(String[] args) {
- SimpleSingleton7.INSTANCE.doSamething();
- }
- }
在枚舉中實例對象INSTANCE是唯一的,所以它是天然的單例模式。
當然,在枚舉對象唯一性的這個特性,還能創(chuàng)建其他的單例對象,例如:
- public enum SimpleSingleton7 {
- INSTANCE;
- private Student instance;
- SimpleSingleton7() {
- instance = new Student();
- }
- public Student getInstance() {
- return instance;
- }
- }
- class Student {
- }
jvm保證了枚舉是天然的單例,并且不存在線程安全問題,此外,還支持序列化。
在java大神Joshua Bloch的經(jīng)典書籍《Effective Java》中說過:
單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。
6 多例模式
我們之前聊過的單例模式,都只會產(chǎn)生一個實例。但它其實還有一個變種,也就是我們接下來要聊的:多例模式。
多例模式顧名思義,它允許創(chuàng)建多個實例。但它的初衷是為了控制實例的個數(shù),其他的跟單例模式差不多。
具體實現(xiàn)代碼如下:
- public class SimpleMultiPattern {
- //持有自己類的引用
- private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
- private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();
- //私有的構(gòu)造方法
- private SimpleMultiPattern() {
- }
- //對外提供獲取實例的靜態(tài)方法
- public static SimpleMultiPattern getInstance(int type) {
- if(type == 1) {
- return INSTANCE1;
- }
- return INSTANCE2;
- }
- }
為了看起來更直觀,我把一些額外的安全相關(guān)代碼去掉了。
有些朋友可能會說:既然多例模式也是為了控制實例數(shù)量,那我們常見的池技術(shù),比如:數(shù)據(jù)庫連接池,是不是通過多例模式實現(xiàn)的?
答:不,它是通過享元模式實現(xiàn)的。
那么,多例模式和享元模式有什么區(qū)別?
- 多例模式:跟單例模式一樣,純粹是為了控制實例數(shù)量,使用這種模式的類,通常是作為程序某個模塊的入口。
- 享元模式:它的側(cè)重點是對象之間的銜接。它把動態(tài)的、會變化的狀態(tài)剝離出來,共享不變的東西。
7 真實使用場景
最后,跟大家一起聊聊,單例模式的一些使用場景。我們主要看看在java的框架中,是如何使用單例模式,給有需要的朋友一個參考。
7.1 Runtimejdk
提供了Runtime類,我們可以通過這個類獲取系統(tǒng)的運行狀態(tài)。
比如可以通過它獲取cpu核數(shù):
- int availableProcessors = Runtime.getRuntime().availableProcessors();
Runtime類的關(guān)鍵代碼如下:
- public class Runtime {
- private static Runtime currentRuntime = new Runtime();
- public static Runtime getRuntime() {
- return currentRuntime;
- }
- private Runtime() {}
- ...
- }
從上面的代碼我們可以看出,這是一個單例模式,并且是餓漢模式。
但根據(jù)文章之前講過的一些理論知識,你會發(fā)現(xiàn)Runtime類的這種單例模式實現(xiàn)方式,顯然不太好。實例對象既沒用final關(guān)鍵字修飾,也沒考慮對象實例化的性能消耗問題。
不過它的優(yōu)點是實現(xiàn)起來非常簡單。
7.2 NamespaceHandlerResolver
spring提供的DefaultNamespaceHandlerResolver是為需要初始化默認命名空間處理器,是為了方便后面做標簽解析用的。
它的關(guān)鍵代碼如下:
- @Nullable
- private volatile Map<String, Object> handlerMappings;
- private Map<String, Object> getHandlerMappings() {
- Map<String, Object> handlerMappings = this.handlerMappings;
- if (handlerMappings == null) {
- synchronized (this) {
- handlerMappings = this.handlerMappings;
- if (handlerMappings == null) {
- if (logger.isDebugEnabled()) {
- logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
- }
- try {
- Properties mappings =
- PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
- if (logger.isDebugEnabled()) {
- logger.debug("Loaded NamespaceHandler mappings: " + mappings);
- }
- handlerMappings = new ConcurrentHashMap<>(mappings.size());
- CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
- this.handlerMappings = handlerMappings;
- }
- catch (IOException ex) {
- throw new IllegalStateException(
- "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
- }
- }
- }
- }
- return handlerMappings;
- }
我們看到它使用了雙重檢測鎖,并且還定義了一個局部變量handlerMappings,這是非常高明之處。
使用局部變量相對于不使用局部變量,可以提高性能。主要是由于 volatile 變量創(chuàng)建對象時需要禁止指令重排序,需要一些額外的操作。
7.3 LogFactory
mybatis提供LogFactory類是為了創(chuàng)建日志對象,根據(jù)引入的jar包,決定使用哪種方式打印日志。具體代碼如下:
- public final class LogFactory {
- public static final String MARKER = "MYBATIS";
- private static Constructor<? extends Log> logConstructor;
- static {
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useSlf4jLogging();
- }
- });
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useCommonsLogging();
- }
- });
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useLog4J2Logging();
- }
- });
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useLog4JLogging();
- }
- });
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useJdkLogging();
- }
- });
- tryImplementation(new Runnable() {
- @Override
- public void run() {
- useNoLogging();
- }
- });
- }
- private LogFactory() {
- // disable construction
- }
- public static Log getLog(Class<?> aClass) {
- return getLog(aClass.getName());
- }
- public static Log getLog(String logger) {
- try {
- return logConstructor.newInstance(logger);
- } catch (Throwable t) {
- throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
- }
- }
- public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
- setImplementation(clazz);
- }
- public static synchronized void useSlf4jLogging() {
- setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
- }
- public static synchronized void useCommonsLogging() {
- setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
- }
- public static synchronized void useLog4JLogging() {
- setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
- }
- public static synchronized void useLog4J2Logging() {
- setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
- }
- public static synchronized void useJdkLogging() {
- setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
- }
- public static synchronized void useStdOutLogging() {
- setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
- }
- public static synchronized void useNoLogging() {
- setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
- }
- private static void tryImplementation(Runnable runnable) {
- if (logConstructor == null) {
- try {
- runnable.run();
- } catch (Throwable t) {
- // ignore
- }
- }
- }
- private static void setImplementation(Class<? extends Log> implClass) {
- try {
- Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
- Log log = candidate.newInstance(LogFactory.class.getName());
- if (log.isDebugEnabled()) {
- log.debug("Logging initialized using '" + implClass + "' adapter.");
- }
- logConstructor = candidate;
- } catch (Throwable t) {
- throw new LogException("Error setting Log implementation. Cause: " + t, t);
- }
- }
- }
這段代碼非常經(jīng)典,但它卻是一個不走尋常路的單例模式。因為它創(chuàng)建的實例對象,可能存在多種情況,根據(jù)引入不同的jar包,加載不同的類創(chuàng)建實例對象。如果有一個創(chuàng)建成功,則用它作為整個類的實例對象。
這里有個非常巧妙的地方是:使用了很多tryImplementation方法,方便后面進行擴展。不然要寫很多,又臭又長的if...else判斷。
此外,它跟常規(guī)的單例模式的區(qū)別是,LogFactory類中定義的實例對象是Log類型,并且getLog方法返回的參數(shù)類型也是Log,不是LogFactory。
最關(guān)鍵的一點是:getLog方法中是通過構(gòu)造器的newInstance方法創(chuàng)建的實例對象,每次請求getLog方法都會返回一個新的實例,它其實是一個多例模式。
7.4 ErrorContext
mybatis提供ErrorContext類記錄了錯誤信息的上下文,方便后續(xù)處理。
那么它是如何實現(xiàn)單例模式的呢?關(guān)鍵代碼如下:
- public class ErrorContext {
- ...
- private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
- private ErrorContext() {
- }
- public static ErrorContext instance() {
- ErrorContext context = LOCAL.get();
- if (context == null) {
- context = new ErrorContext();
- LOCAL.set(context);
- }
- return context;
- }
- ...
- }
我們可以看到,ErrorContext跟傳統(tǒng)的單例模式不一樣,它改良了一下。它使用了餓漢模式,并且使用ThreadLocal,保證每個線程中的實例對象是單例的。這樣看來,ErrorContext類創(chuàng)建的對象不是唯一的,它其實也是多例模式的一種。
7.5 spring的單例
以前在spring中要定義一個bean,需要在xml文件中做如下配置:
- <bean id="test" class="com.susan.Test" init-method="init" scope="singleton">
在bean標簽上有個scope屬性,我們可以通過指定該屬性控制bean實例是單例的,還是多例的。如果值為singleton,代表是單例的。當然如果該參數(shù)不指定,默認也是單例的。如果值為prototype,則代表是多例的。
在spring的AbstractBeanFactory類的doGetBean方法中,有這樣一段代碼:
- if (mbd.isSingleton()) {
- sharedInstance = getSingleton(beanName, () -> {
- return createBean(beanName, mbd, args);
- });
- bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
- } else if (mbd.isPrototype()) {
- Object prototypeInstance = createBean(beanName, mbd, args);
- bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
- } else {
- ....
- }
這段代碼我為了好演示,看起來更清晰,我特地簡化過的。它的主要邏輯如下:
- 判斷如果scope是singleton,則調(diào)用getSingleton方法獲取實例。
- 如果scope是prototype,則直接創(chuàng)建bean實例,每次會創(chuàng)建一個新實例。
- 如果scope是其他值,則允許我們自定bean的創(chuàng)建過程。
其中g(shù)etSingleton方法主要代碼如下:
- public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
- Assert.notNull(beanName, "Bean name must not be null");
- synchronized (this.singletonObjects) {
- Object singletonObject = this.singletonObjects.get(beanName);
- if (singletonObject == null) {
- singletonObject = singletonFactory.getObject();
- if (newSingleton) {
- addSingleton(beanName, singletonObject);
- }
- }
- return singletonObject;
- }
- }
有個關(guān)鍵的singletonObjects對象,其實是一個ConcurrentHashMap集合:
- private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
getSingleton方法的主要邏輯如下:
- 根據(jù)beanName先從singletonObjects集合中獲取bean實例。
- 如果bean實例不為空,則直接返回該實例。
- 如果bean實例為空,則通過getObject方法創(chuàng)建bean實例,然后通過addSingleton方法,將該bean實例添加到singletonObjects集合中。
- 下次再通過beanName從singletonObjects集合中,就能獲取到bean實例了。
在這里spring是通過ConcurrentHashMap集合來保證對象的唯一性。
最后留給大家?guī)讉€小問題思考一下:
1. 多例模式 和 多對象模式有什么區(qū)別?
2. java框架中有些單例模式用的不規(guī)范,我要參考不?
3. spring的單例,只是結(jié)果是單例的,但完全沒有遵循單例模式的固有寫法,它也算是單例模式嗎?