單例模式Singleton
面試的時候,問到許多年輕的Android開發他所會的設計模式是什麼,基本上都會提到單例模式,但是對
單例模式也是一知半解,在Android開發中我們經常會運用單例模式,是以我們還是要更了解單例模式才對。
定義:保證一個類僅有一個執行個體,并提供一個通路它的全局通路點。
單例模式結構圖:
單例模式有多種寫法各有利弊,現在
我們來看看各種模式寫法。
1. 餓漢模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
這種方式在類加載時就完成了初始化,是以類加載較慢,但擷取對象的速度快。 這種方式基于類加載機制避免了多線程的同步問題,
但是也不能确定有其他的方式(或者其他的靜态方法)導緻類裝載,這時候初始化instance顯然沒有達到懶加載的效果。
2. 懶漢模式(線程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懶漢模式申明了一個靜态對象,在使用者第一次調用時初始化,雖然節約了資源,但第一次加載時需要執行個體化,
反映稍慢一些,而且在多線程不能正常工作。
3. 懶漢模式(線程安全)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這種寫法能夠在多線程中很好的工作,但是每次調用getInstance方法時都需要進行同步,造成不必要的同步開銷,
而且大部分時候我們是用不到同步的,是以不建議用這種模式。
4. 雙重檢查模式 (DCL)
public class Singleton {
private volatile static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return singleton;
}
}
這種寫法在getSingleton方法中對singleton進行了兩次判空,第一次是為了不必要的同步,第
二次是在singleton等于null的情況下才建立執行個體。在這裡用到了volatile關鍵字,不了解volatile關鍵
字的可以檢視Java多線程(三)volatile域這篇文章,在這篇文章我也提到了雙重
檢查模式是正确使用volatile關鍵字的場景之一。
在這裡使用volatile會或多或少的影響性能,但考慮
到程式的正确性,犧牲這點性能還是值得的。 DCL優點是資源使用率高,第一次執行getInstance時單例
對象才被執行個體化,效率高。缺點是第一次加載時反應稍慢一些,在高并發環境下也有一定的缺陷,雖然
發生的機率很小。DCL雖然在一定程度解決了資源的消耗和多餘的同步,線程安全等問題,但是他還是在
某些情況會出現失效的問題,也就是DCL失效,在《java并發程式設計實踐》一書建議用**靜态内部類單例模
式**來替代DCL。
5. 靜态内部類單例模式
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
第一次加載Singleton類時并不會初始化sInstance,隻有第一次調用getInstance方法時虛拟機加載
SingletonHolder 并初始化sInstance ,這樣不僅能確定線程安全也能保證Singleton類的唯一性,是以
推薦使用靜态内部類單例模式。
6. 枚舉單例
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
預設枚舉執行個體的建立是線程安全的,并且在任何情況下都是單例,上述講的幾種單例模式實作中,有一種
情況下他們會重新建立對象,那就是反序列化,将一個單例執行個體對象寫到磁盤再讀回來,進而獲得了一個
執行個體。反序列化操作提供了readResolve方法,這個方法可以讓開發人員控制對象的反序列化。在上述的幾
個方法示例中如果要杜絕單例對象被反序列化是重新生成對象,就必須加入如下方法:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
枚舉單例的優點就是簡單,但是大部分應用開發很少用枚舉,可讀性并不是很高,不建議用。
7. 使用容器實作單例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;
}
}
public static Object getService(String key) {
return objMap.get(key) ;
}
}
用SingletonManager 将多種的單例類統一管理,在使用時根據key擷取對象對應類型的對象。這種方式
使得我們可以管理多種類型的單例,并且在使用時可以通過統一的接口進行擷取操作,降低了使用者的使
用成本,也對使用者隐藏了具體實作,降低了耦合度。
總結
到這裡七中寫法都介紹完了,至于選擇用哪種形式的單例模式,取決于你的項目本身,是否
是有複雜的并發環境,還是需要控制單例對象的資源消耗。
基于volatile的雙重檢查鎖(double-checked locking)
public class Singleton {
//private static Singleton instance = null;
//防止DCL失效問題,保證instance對象每次都是從住記憶體中讀取
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { //避免不必要的同步
synchronized (Singleton.class) {
//在instance = null下建立執行個體
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
缺點
:
- JDK1.5以上版本才有volatile。
優點
- 資源使用率高,第一次執行getInstance()時才會被執行個體化,效率高。
隻是看起來很完美
這種看起來很完美的優化技巧就是double-checked locking,但遺憾地告訴你,根據JLS規範,上面的代碼不可靠!線程有可能得到一個不為null,但是構造不完全的對象。
Why?
造成不可靠的原因是編譯器為了提高執行效率的指令重排。隻要認為在單線程下是沒問題的,它就可以進行亂序寫入!以保證不要讓cpu指令流水線中斷。
指令重排
為了提高代碼的執行效率,JVM會将執行頻率高的代碼編譯成機器碼;而對于頻率不高的代碼則仍然采用解釋執行。
常見的編譯優化方式有:
方法内聯:免去參數、傳回值傳遞過程
去虛拟化:接口的方法隻有一個實作類,進行方法内聯
備援消除:運作時去掉無用代碼
還有一些編譯優化根據“逃逸分析”技術
标量替換:User u=new User(“zhang3”,18)
String n=“zhang3” int age=18,節省記憶體
棧上配置設定:逃逸對象直接在棧上配置設定,快速,GC及時
同步削除:去掉不必要的同步
new Instance()到底發生了什麼?
memory = allocate(); //1:配置設定對象的記憶體空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設定instance指向剛配置設定的記憶體位址
上面的僞代碼中2、3步可能重排
解決方案1
Java5以後的版本,可以利用volatile關鍵字。
在java5以前,volatile原語不怎麼強大,隻能保證對象的可見性
但在java5之後,volatile語義加強了,被volatile修飾的對象,将禁止該對象上的讀寫指令重排序
這樣,就保證了線程B讀對象時,已經初始化完全了
解決方案2
這也是官方比較推薦的一種方案(effective java 2nd)
點選(此處)折疊或打開
public class InstanceHolder{
private Instance(){}
//Lazy initialization holder class idiom for static fields
private static class Inner{
private static final Instance ins = new Instance()
}
public static Instance getInstance(){
return Inner.ins;
}
}
原理:一個類隻有在被使用時才會初始化,而類初始化過程是非并行的,這些都由JLS能保證。
靜态内部類單例模式
在《Java 并發程式設計實踐》中提及了DCL失效的問題(上面基于volatile的雙重檢查鎖例子中注釋的部分),建議使用如下代碼替換:
public class Singleton {
private static final Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
sInstance = new Singleton();
}
}
優點
- 不僅保證線程安全,而且也保證了單例對象的唯一性,同時也延遲了單例的執行個體化,是以這才是
的單例模式實作方式。推薦使用
枚舉單例
public class SingletonEnum {
INSTANCE;
public void doSomething() {
//do something;
}
}
優點
- 枚舉在Java中于普通的類一樣,可以有字段,方法。 預設枚舉建立的執行個體是線程安全的。
- 使用enum實作的單例自帶防序列化。
然而上述的幾種單例模式實作中在反序列化時會重新建立對象。要杜絕單例對象重新生成對象,必須要加入如下方法:
private Object readResolve() throws ObjectStreamException {
return sInstance;
}
參考資料
- [1]《Android源碼設計模式解析和實戰》
- [2]《Java 并發程式設計實踐》