建立型:單例模式及相關應用
文章目錄
單例模式(Singleton)
保證一個類僅有一個例項,並提供一個全域性訪問點
適用場景:想確保任何情況下都絕對只有一個例項。
優缺點
優點:在記憶體裡只有一個例項,減少了記憶體開銷;可以避免對資源的多重佔用;設定全域性訪問點,嚴格控制訪問。
缺點:可擴充套件性較差。
重點
- 私有構造器
- 執行緒安全
- 延遲載入
- 序列化和反序列化
- 反射
懶漢式實現
執行緒不安全
以下實現中延遲了lazySingleton的例項化,因此如果沒有使用該類,那麼就不會例項化lazySingleton,從而節約了資源。
但這種實現是執行緒不安全的,在多執行緒的環境下多個執行緒有可能同時判斷if(lazySingleton == null)
true
而進行例項化,導致多次例項化lazySingleton。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
synchronized關鍵字
要想其變為執行緒安全的,第一種方式是在getInstance()
方法加上synchronized
關鍵字,使這個方法變為同步方法:
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
由於這個方法是靜態方法,因此這個鎖將鎖住這個類,等效於以下程式碼:
public static LazySingleton getInstance(){
synchronized (LazySingleton.class){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
通過這種方式,雖然解決了懶漢式在多執行緒環境下的同步問題,但由於同步鎖消耗的資源較多,且鎖的範圍較大,對效能有一定影響,因此還需要進行演進。
雙重校驗鎖
當lazyDoubleCheckSingleton就算沒有被例項化時,synchronized
關鍵字也保證了不會出現同步問題,例如,如果兩個執行緒同時判斷第一個if(lazyDoubleCheckSingleton == null)
為true
,其中一個執行緒會進入到第二個if(lazyDoubleCheckSingleton == null)
並開始例項化lazyDoubleCheckSingleton,而另一個執行緒則被阻塞直到前一個程序釋放鎖。一旦前一個執行緒例項化完並釋放鎖,被阻塞的執行緒將進入第二個if(lazyDoubleCheckSingleton == null)
且判斷為false
。之後,由於lazyDoubleCheckSingleton已經被例項化過,再有執行緒呼叫此方法都會在第一個if(lazyDoubleCheckSingleton == null)
就判斷為false
,不會再進行加鎖操作。
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
這種實現依然存在問題,對於lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
這一行程式碼其實是分為以下三步執行的:
- 分配記憶體給這個物件
- 初始化物件
- 設定lazyDoubleCheckSingleton指向剛分配的記憶體地址
但是JVM為了優化指令,提高程式執行效率,會進行指令重排序,指令順序有可能由1->2->3變為1->3->2,這在單執行緒下不會出現問題,但是在多執行緒下會導致一個執行緒獲得還沒有被初始化的例項。例如,一個執行緒已經執行到了lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
這一行,且完成了1->3這兩步,即lazyDoubleCheckSingleton已經不為null,但還沒有進行初始化,此時另一個執行緒在第一個if(lazyDoubleCheckSingleton == null)
判斷為false
後便將還未被初始化的lazyDoubleCheckSingleton返回,從而產生問題。
要解決指令重排序導致的問題,第一種方式是使用volatile
關鍵字禁止JVM進行指令重排序:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
//...
}
}
靜態內部類
另一種解決指令重排序所導致的問題的方式是使用靜態內部類讓其它執行緒看不到這個執行緒的指令重排序:
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
當StaticInnerClassSingleton類載入時,靜態內部類InnerClass還不會載入進記憶體,只有呼叫getInstance()
方法使用到了InnerClass.staticInnerClassSingleton
時才會載入。在多執行緒環境下,只有一個執行緒能獲得Class物件的初始化鎖,從而載入StaticInnerClassSingleton類,也就是這時候完成staticInnerClassSingleton的例項化,另一個執行緒此時只能在這個Class物件的初始化鎖上等待。因此,由於等待的執行緒是看不見指令重排序的過程的,所以指令重排的順序不會有任何影響。
餓漢式實現
餓漢式即當類載入的時候就完成例項化,避免了同步問題,但同時也因為沒有延遲例項化的特性而導致資源的浪費。
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
以上程式碼與以下程式碼等效:
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
單例模式存在的問題
序列化破壞單例模式
通過對Singleton的序列化與反序列化得到的物件是一個新的物件,這就破壞了Singleton的單例性。
public class Test {
public static void main(String[] args){
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (EnumInstance) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
之所以會如此,是因為序列化會通過反射呼叫無引數的構造方法建立一個新的物件。要解決這個問題很簡單:只要在Singleton類中定義readResolve即可:
public class HungrySingleton implements Serializable {
//...
private Object readResolve(){
return hungrySingleton;
}
//...
}
反射攻擊
通過反射可以開啟Singleton的構造器許可權,由此例項化一個新的物件。
public class Test {
public static void main(String[] args){
Class objectClass = HungrySingleton.class;
Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
對於餓漢式,由於是在類載入的時候就例項化物件了,因此要解決反射攻擊問題,可以在構造器內部加一個判斷用來防禦,這樣當反射呼叫構造器的時候hungrySingleton已經存在,不會再進行例項化並丟擲異常:
public class HungrySingleton implements Serializable {
//...
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
//...
}
而對於懶漢式,即使加上了上面的防禦程式碼,依然可以通過調整順序即先使用反射建立例項,再呼叫getInstance()
建立例項來得到不止一個該類的物件。
列舉實現
列舉類是實現單例的最佳方式,其在多次序列化再進行反序列化之後不會得到多個例項,也可以防禦反射攻擊。這部分的處理是由ObjectInputStream
和Constructor
這兩個類實現的。
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
容器實現
如果系統中單例物件特別多,則可以考慮使用一個容器把所有單例物件統一管理,但是是執行緒不安全的。
public class ContainerSingleton {
private static Map<String, Object> singletonMap = new HashMap<String, Object>();
private ContainerSingleton(){
}
public static void putInstance(String key, Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
Runtime中的應用
檢視java.lang
包下的Runtime
類:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
//...
}
這裡的currentRuntime在類載入的時候就例項化好了,屬於餓漢式單例模式。
Spring中的應用
檢視org.springframework.beans.factory.config
包下的AbstractFactoryBean
:
public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
//...
public final T getObject() throws Exception {
if (this.isSingleton()) {
return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance();
} else {
return this.createInstance();
}
}
private T getEarlySingletonInstance() throws Exception {
Class<?>[] ifcs = this.getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
} else {
if (this.earlySingletonInstance == null) {
this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler());
}
return this.earlySingletonInstance;
}
}
//...
}
在getObject()
方法中,先判斷這個物件是否為單例的,如果不是則直接建立;如果是單例的,那麼判斷是否被初始化過,如果被初始化了則直接返回,沒有的話則呼叫getEarlySingletonInstance()
方法獲取早期的單例物件,如果早期的單例物件不存在,則通過代理來獲取。
參考資料
- 弗里曼. Head First 設計模式 [M]. 中國電力出版社, 2007.
- 慕課網java設計模式精講 Debug 方式+記憶體分析
- CS-NOTE 設計模式