為什么80%的碼農都做不了架構師?>>> ??
?????? 單例模式作為設計模式中最簡單的一種,是一個被說爛了的東西。但是在項目中還是會發現關于單例模式的一些錯誤實現,可見單例也并不是我們想象的那么簡單。最近陸陸續續看了幾篇關于單例的博客,很受啟發,所以覺得有必要總結一下(只涉及常用的雙重檢查鎖定、靜態內部類、枚舉三種單例實現方法),本文將從安全和性能兩個方面闡述我對單例模式三種最佳實踐的理解。不當之處,請 指正。
1、雙重校驗鎖定
?
//Double Checked locking
public class SingletonByDCL
{private Map<Integer, String> configMap;private volatile static SingletonByDCL instance = null;private SingletonByDCL(Map<Integer, String> configMap){this.configMap = configMap;}public static SingletonByDCL getInstance(){SingletonByDCL inst = instance;if (null == inst){synchronized (SingletonByDCL.class){inst = instance;if (null == inst){inst = new SingletonByDCL(ConfigReader.configMap);instance = inst;}}}return inst;}
}
?
通過synchronized和volatile實現了線程安全。其中需要注意的是實例變量一定要用volatile修飾。原因可參考Java 單例真的寫對了么?。當單例對象需要被序列化時,就應該考慮單例實現的序列化安全,在Singleton中定義readResolve方法,并在該方法中指定要返回的對象的生成策略,就可以方式單例被破壞,原理可參考單例與序列化的那些事兒。可能會有人使用反射強行調用我們的私有構造器,為了保證訪問安全,可以修改構造器,讓它在創建第二個實例的時候拋異常。
2、靜態內部類
?
public class SingletonByInnerStaticClass
{private Map<Integer, String> configMap;private SingletonByInnerStaticClass(Map<Integer, String> configMap){this.configMap = configMap;}private static class SingletonHolder{private static SingletonByInnerStaticClass instance = new SingletonByInnerStaticClass(ConfigReader.configMap);}public static SingletonByInnerStaticClass getInstance(){return SingletonHolder.instance;}
}
?
線程安全,這是 Java 運行環境自動給保證的,在加載的時候,會自動隱形的同步。在訪問對象的時候,不需要同步 Java 虛擬機又會自動取消同步。對于序列化安全和訪問安全的保證,解決方法同“雙重檢查鎖定”。
3、枚舉
?
public enum SingletonByEnum
{instanse(ConfigReader.configMap);private Map<Integer,String> configMap;private SingletonByEnum(Map<Integer,String> configMap){this.configMap = configMap;}public Map<Integer, String> getConfigMap(){return configMap;}
}
當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的。所以,創建一個enum類型(可用javap查看enum編譯后的class文件,從而了解enum包含哪些靜態資源)是線程安全的。為了保證枚舉類型像Java規范中所說的那樣,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定,從而保證序列化安全。
?
private static void testSingletonByEnum() throws IOException, FileNotFoundException, ClassNotFoundException{// 序列化ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempfile"));oos.writeObject(SingletonByEnum.instanse);// 反序列化ObjectInputStream ois = new ObjectInputStream(new FileInputStream("tempfile"));SingletonByEnum s = (SingletonByEnum) ois.readObject();// 判斷是否為同一對象if (s == SingletonByEnum.instanse){System.out.println("創建的是同一個實例");} else{System.out.println("創建的不是同一個實例");}}
?
創建的是同一個實例
?
?
?
同時java.lang.reflect.Constructor的newInstance()方法中有如下代碼,禁止了通過反射構造枚舉對象,所以枚舉可以保證訪問安全。關于枚舉如何保證線程安全和序列化安全,可參考深度分析 Java 的枚舉類型:枚舉的線程安全性及序列化問題
if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
4、三種方法的性能比較
三種方法都實現了延遲加載,在8線程同時調用,每個線程調用100000000次的情況下時間對比如下:
SingletonByDCL—》845
SingletonByEnum—》90
SingletonByInnerStaticClass—》89
具體測試代碼參見附件工程中的com.zjg.perf.PerformanceTest2
綜上所述,Effective Java中推薦使用的枚舉實現單例無論從安全還是性能都是有道理的。當然代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不同的平臺、不同的開發環境(尤其是jdk版本)下,也就會有不同的最優解。比如枚舉在Android平臺上卻是不被推薦的。在這篇Android Training中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如雙重檢查鎖法,不能在jdk1.5之前使用,而在Android平臺上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語義問題,還加入了不少鎖優化,使得多線程同步的開銷降低不少)。
參考文章:
http://blog.jobbole.com/94074/? 深度分析 Java 的枚舉類型:枚舉的線程安全性及序列化問題
http://www.importnew.com/18872.html? 你真的會寫單例模式嗎——Java實現
http://www.hollischuang.com/archives/1144? 單例與序列化的那些事兒
http://www.race604.com/java-double-checked-singleton/?utm_source=tuicool&utm_medium=referral Java 單例真的寫對了么?
demo工程:https://git.oschina.net/zjg23/SingletonDemo