設計模式實戰——開發中常用到的單例模式
- 2020 年 3 月 2 日
- 筆記
本系列博客是自己在學習設計模式過程中收集整理的文章集合,其他文章參看設計模式傳送門
單例模式簡介
單例模式的目的是保證系統中只有類的一個實例對象,並且提供一個全局的入口點來獲取並使用這個實例對象。
使用單例模式可以防止用戶「胡亂」創建對象,耗費內存。而且有些對象從邏輯上來講一個系統中只應該存在一個,比如說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