源碼中的設計模式–單例模式

一、模式入場

單例模式在眾多的設計模式中應該是最簡單的一個,但是要掌握的點也不少。先看下《head first 設計模式》中給出的釋義,

單件模式 確保一個類只有一個實例,並提供一個全局訪問點。

 下面對這個釋義進行逐字解釋。單件可以稱之為單例其實是一個意思。這個釋義給出了單例模式中重要的兩點,

  1. 一個類只有一個實例;
  2. 提供一個全局的訪問點;

先說第一條,一個類只有一個實例,在一個系統中會有很多類,如下面的訂單類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(){
        
    }
    
}

上面便是懶漢模式。

對比餓漢模式和懶漢模式,可以發現其區別在於什麼時機調用私有的構造方法生成實例,區分方式是,懶漢模式只有在調用全局訪問點的時候才會生成實例,而餓漢模式則在類載入的時候便會生成實例,所以根據生成實例的時機去區分餓漢和懶漢就容易的多了。

這裡想留幾個思考問題,

  1. 上面的懶漢模式有問題嗎?
  2. 生成實例的方式除了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管理了默認就是單例的。

 

好了,本次就說這麼多,我們下次見!