源碼中的設計模式–單例模式
一、模式入場
單例模式在眾多的設計模式中應該是最簡單的一個,但是要掌握的點也不少。先看下《head first 設計模式》中給出的釋義,
單件模式 確保一個類只有一個實例,並提供一個全局訪問點。
下面對這個釋義進行逐字解釋。單件可以稱之為單例其實是一個意思。這個釋義給出了單例模式中重要的兩點,
- 一個類只有一個實例;
- 提供一個全局的訪問點;
先說第一條,一個類只有一個實例,在一個系統中會有很多類,如下面的訂單類Order
public class Order { private String orderId; private BigDecimal orderAmount; private String orderPerson; }
那麼現在就有一個問題,如何保證一個類只有一個實例,最先想到的是強制要求這個系統中的所有開發人員,在開發的時候只能實例一次,一個人實例化了,另一個人就不能實例化,這是一個辦法,但是卻不可行,因為我要實例化這個類的時候總不能先問下其他人,你們實例化過沒有,只有其他人沒有實例化的前提下你才可以實例化,而且你還要告知其他人以後誰都不能再實例化Order了,這樣是不是太傻了,並且效率也太低了,純靠人為約定肯定是行不通了。有沒有其他的方法吶,答案是有的。
大家都知道,通常情況下實例化一個類,最簡單的方式就是new一個,誰說我沒有女朋友,new一個啊。現在我們也new一個Order,但是我們發現任何一個人都可以new,這怎麼能保證只有一個實例,那麼我把它的構造方法設為私有的,這樣你們都不能new了吧,能new的只有一個拉,也就是在Order的內部可以new,這就可以保證一個類只有一個實例了,因為只有在類的內部才可以調用其私有的構造方法,其他地方想調用「對不起,您沒有訪問許可權」。
好了上面通過把類的構造方法設為私有的,保證了一個類只有一個實例,那麼如何才能訪問到這個實例吶,假設現在的程式碼是這樣的,
public class Order { private String orderId; private BigDecimal orderAmount; private String orderPerson; /** * 私有的構造方法 */ private Order(){ } /** * 通過私有構造方法生成的唯一實例 */ Order order=new Order(); }
我們現在就要訪問到通過私有構造方法生成的實例order,怎麼才能訪問到吶?提供一個靜態方法,靜態方法是類級別的,不依賴於實例,可以通過類名.靜態方法名的方式訪問,如下
public class Order { private String orderId; private BigDecimal orderAmount; private String orderPerson; /** * 私有的構造方法 */ private Order(){ } /** * 通過私有構造方法生成的唯一實例 */ private static Order order=new Order(); /** * 全局訪問點,靜態方法 * @return */ public static Order getInstance(){ return order; } }
通過提供一個靜態方法,由靜態方法返回該唯一實例,由於靜態方法中要引用order實例,所以該實例也必須是靜態的,靜態方法是公共的,那麼order也應設為私有的,這樣就提供了一個全局的訪問點,任何地方想使用這個唯一實例調用該靜態方法即可。
好了,到目前為止你已經掌握了一些單例模式的方法。
二、深入單例模式
一般情況下,單例模式分為懶漢和餓漢兩種模式,這兩種模式很容易記混,我這裡有一個好的記憶方式,下面會提到。
上面的演示中其實就是餓漢模式,下面看懶漢模式,
public class Singleton { private static Singleton singleton; /** * 全局訪問點,提供singleton實例的唯一訪問 * @return */ public static Singleton getInstance(){ if(singleton==null){ singleton=new Singleton(); } return singleton; } /** * 唯一的私有構造函數,提供唯一的實例 */ private Singleton(){ } }
上面便是懶漢模式。
對比餓漢模式和懶漢模式,可以發現其區別在於什麼時機調用私有的構造方法生成實例,區分方式是,懶漢模式只有在調用全局訪問點的時候才會生成實例,而餓漢模式則在類載入的時候便會生成實例,所以根據生成實例的時機去區分餓漢和懶漢就容易的多了。
這裡想留幾個思考問題,
- 上面的懶漢模式有問題嗎?
- 生成實例的方式除了new還有其他方式嗎?
三、追尋源碼
在這個模組中想通過源碼來學習下單例模式,讓大家看看優秀的人是怎麼使用單例模式的。
1、ErrorContext
在經常使用的mybatis的源碼中有ErrorContext這樣的一個類,下面貼出ErrorContext中的部分程式碼
package org.apache.ibatis.executor; /** * @author Clinton Begin */ public class ErrorContext { private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n"); private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); private ErrorContext stored; private String resource; private String activity; private String object; private String message; private String sql; private Throwable cause;
//私有構造方法 private ErrorContext() { } //全局訪問點 public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } }
在上面的程式碼中,ErrorContext有私有的構造方法,同時具有instance()方法提供全局唯一訪問點,而且從方法我們知道這應該是一個懶漢模式。
再看下instance()方法,細心的小夥伴會說,這個不是全局唯一訪問點,這是從Local變數中取的ErrorContext對象,而Local是ThreadLocal級別的,不是整個系統只有一份啊,這裡我要說,大家不必局限於字眼,我們也可以把ThreadLocal看成是一個系統啊,它畢竟是屬於執行緒級別的啊,要真正掌握的是單例的本質,可以仔細體會下。
2、LogFactory
同樣是在mybatis的源碼中有LogFactory類,局部程式碼如下,
public final class LogFactory { /** * Marker to be used by logging implementations that support markers */ public static final String MARKER = "MYBATIS"; private static Constructor<? extends Log> logConstructor; static { tryImplementation(LogFactory::useSlf4jLogging); tryImplementation(LogFactory::useCommonsLogging); tryImplementation(LogFactory::useLog4J2Logging); tryImplementation(LogFactory::useLog4JLogging); tryImplementation(LogFactory::useJdkLogging); tryImplementation(LogFactory::useNoLogging); } //私有構造方法 private LogFactory() { // disable construction } }
在該類中可以看到有私有方法,但是卻沒有提供全局的訪問入口,您會說這也是單例模式嗎,我說算,這個類符合單例的定義啊,具有私有構造方法肯定只有一個實例,但是卻沒有創建實例,這個類中其他的方法均是工具方法,為什麼不提供全局訪問入口,答案是用不到,用不到所以就不提供了啊。
3、單例bean
現在開發中用的最多的就是springboot,springboot的基礎是spring,把類交給spring管理使用@Autowired就搞定了,您是不是也知道spring中的bean默認都是單例的,沒錯spring中使用了單例模式,有同學就說了,在平時寫的類中也沒有提供私有的構造方法啊,是如何保證單例的吶,還記得上邊的思考問題嗎?除了使用new的方式還有其他的方式,spring使用的是反射的方式,具體程式碼先不貼了,太多了,一時半會分析不明白,那全局的訪問方式吶,答案是beanFactory
在beanFactory中定義了很多getBean的方法,調用這些方法便會返回一個單例bean,那這些單例bean存儲在什麼地方那,答案在DefaultSingletonBeanRegistry中,該類中有一個singletonObjects屬性,該屬性中就存著所有spring管理的單例bean,
老鐵們,看到了吧,這也是單例模式,但這個單例模式比平時自己寫的單例模式高明多了,在生成唯一實例時使用的是反射,在提供全局的訪問入口的時候,是從hashmap中返回的,比自己寫個靜態方法高明多了。
有的小夥伴會問,一個類被spring管理也沒提供私有方法,是不是可以自己new啊,是可以的,隨便new多少個都行,但是只要被spring管理了默認就是單例的。
好了,本次就說這麼多,我們下次見!