­

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

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

單例模式簡介

單例模式的目的是保證系統中只有類的一個實例對象,並且提供一個全局的入口點來獲取並使用這個實例對象。

使用單例模式可以防止用戶「胡亂」創建對象,耗費內存。而且有些對象從邏輯上來講一個系統中只應該存在一個,比如說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