1. 程式人生 > >設計模式實戰——開發中常用到的單例模式

設計模式實戰——開發中常用到的單例模式

本系列部落格是自己在學習設計模式過程中收集整理的文章集合,其他文章參看設計模式傳送門

單例模式簡介

單例模式的目的是保證系統中只有類的一個例項物件,並且提供一個全域性的入口點來獲取並使用這個例項物件。

使用單例模式可以防止使用者“胡亂”建立物件,耗費記憶體。而且有些物件從邏輯上來講一個系統中只應該存在一個,比如說Runtime類,使用單例模式也能很好的保證這一點。

本文介紹幾個我們平時開發過程中常用到的單例模式場景,來加深我們對單例模式的理解。

JDK中的單例模式

Runtime類封裝了Java執行時的環境。每一個java程式實際上都是啟動了一個JVM程序,那麼每個JVM程序都是對應這一個Runtime例項,此例項是由JVM為其例項化的。每個 Java 應用程式都有一個 Runtime 類例項,使應用程式能夠與其執行的環境相連線。

由於Java是單程序的,所以,在一個JVM中,Runtime的例項應該只有一個。所以應該使用單例來實現。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}

以上程式碼為JDK中Runtime類的部分實現,可以看到,這其實是餓漢式單例模式。在該類第一次被classloader載入的時候,這個例項就被創建出來了。

Spring中的單例模式

我們知道在Spring中預設注入的Bean都是單例,那麼Spring中的單例是怎麼生成的呢?我們來看下Spring生成Bean的程式碼。


@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
         singletonObject = this.earlySingletonObjects.get(beanName);
         if (singletonObject == null && allowEarlyReference) {
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
               singletonObject = singletonFactory.getObject();
               this.earlySingletonObjects.put(beanName, singletonObject);
               this.singletonFactories.remove(beanName);
            }
         }
      }
   }
   return singletonObject;
}

spring依賴注入時,使用了雙重判斷加鎖的單例模式,首先從快取MAP中獲取bean例項,如果為null,對快取map加鎖,然後再從快取中獲取bean,如果繼續為null,就建立一個bean。

Spring並沒有使用私有構造方法來建立bean,而是通過singletonFactory.getObject()返回具體beanName對應的ObjectFactory來建立bean。實際上是呼叫了AbstractAutowireCapableBeanFactory的doCreateBean方法,返回了BeanWrapper包裝並建立的bean例項。

MyBatis中的單例模式

1. ErrorContext

ErrorContext是用在每個執行緒範圍內的單例,用於記錄該執行緒的執行環境錯誤資訊。

public class ErrorContext {

  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();

  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;
  }

}

建構函式是private修飾,具有一個static的區域性instance變數和一個獲取instance變數的方法,在獲取例項的方法中,先判斷是否為空如果是的話就先建立,然後返回構造好的物件。

只是這裡有個有趣的地方是,LOCAL的靜態例項變數使用了ThreadLocal修飾,也就是說它屬於每個執行緒各自的資料,而在instance()方法中,先獲取本執行緒的該例項,如果沒有就建立該執行緒獨有的ErrorContext。

也就是說ErrorContext是執行緒範圍內的單例,而不是全域性範圍內(JVM內)的單例。

2. VFS


public abstract class VFS {
  private static final Log log = LogFactory.getLog(VFS.class);

  /** The built-in implementations. */
  public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };

  /** The list to which implementations are added by {@link #addImplClass(Class)}. */
  public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();

  /** Singleton instance. */
  private static VFS instance;

  /**
   * Get the singleton {@link VFS} instance. If no {@link VFS} implementation can be found for the
   * current environment, then this method returns null.
   */
  @SuppressWarnings("unchecked")
  public static VFS getInstance() {
    if (instance != null) {
      return instance;
    }
}

VFS是MyBatis中提供的檔案系統類,存在感比較低。但是我們看下這個類的原始碼的話,的確是很標準的單例模式。

Log4j中的單例

Log4jLoggerFactory建立Logger時也是用的單例模式。程式碼如下:


private static ConcurrentMap<String, Logger> log4jLoggers = new ConcurrentHashMap();

    Log4jLoggerFactory() {
    }

    public static Logger getLogger(String name) {
        Logger instance = (Logger)log4jLoggers.get(name);
        if (instance != null) {
            return instance;
        } else {
            Logger newInstance = new Logger(name);
            Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }

    public static Logger getLogger(String name, LoggerFactory loggerFactory) {
        Logger instance = (Logger)log4jLoggers.get(name);
        if (instance != null) {
            return instance;
        } else {
            Logger newInstance = loggerFactory.makeNewLoggerInstance(name);
            Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }

PS:有一個問題,不同的多個Logger向同一個檔案中打日誌時,是怎麼保證高效並且執行緒安全的???

參考

  • https://my.oschina.net/u/1024107/blog/1836488