確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例谓谦。
單例模式的使用很廣泛贫橙,比如:線程池(threadpool)、緩存(cache)反粥、對話框卢肃、處理偏好設置、和注冊表(registry)的對象才顿、日志對象莫湘,充當打印機、顯卡等設備的驅(qū)動程序的對象等郑气,這些類的對象只能有一個實例,如果制造出多個實例尾组,就會導致很多問題的產(chǎn)生忙芒,程序的行為異常,資源使用過量演怎,或者不一致的結(jié)果等
特點
- 構造函數(shù)不對外開放匕争,一般為private;
- 通過一個靜態(tài)方法或者枚舉返回單例類對象爷耀;
- 確保單例類的對象有且只有一個甘桑,尤其是在多線程的環(huán)境下;
- 確保單例類對象在反序列化時不會重新構建對象歹叮。
- 通過將單例類構造函數(shù)私有化跑杭,使得客戶端不能通過 new 的形式手動構造單例類的對象。單例類會暴露一個共有靜態(tài)方法咆耿,客戶端需要調(diào)用這個靜態(tài)方法獲取到單例類的唯一對象德谅,在獲取到這個單例對象的過程中需要確保線程安全,即在多線程環(huán)境下構造單例類的對象也是有且只有一個萨螺,這是單例模式較關鍵的一個地方窄做。
主要優(yōu)點
- 單例模式提供了對唯一實例的受控訪問愧驱。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它椭盏。
- 由于在系統(tǒng)內(nèi)存中只存在一個對象组砚,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象單例模式無疑可以提高系統(tǒng)的性能掏颊。
- 允許可變數(shù)目的實例糟红。基于單例模式我們可以進行擴展乌叶,使用與單例控制相似的方法來獲得指定個數(shù)的對象實例盆偿,既節(jié)省系統(tǒng)資源,又解決了單例對象共享過多有損性能的問題准浴。
主要缺點
由于單例模式中沒有抽象層事扭,因此單例類的擴展有很大的困難。
單例類的職責過重乐横,在一定程度上違背了“單一職責原則”句旱。因為單例類既充當了工廠角色,提供了工廠方法晰奖,同時又充當了產(chǎn)品角色,包含一些業(yè)務方法腥泥,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起匾南。
現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術蛔外,因此蛆楞,如果實例化的共享對象長時間不被利用,系統(tǒng)會認為它是垃圾夹厌,會自動銷毀并回收資源豹爹,下次利用時又將重新實例化,這將導致共享的單例對象狀態(tài)的丟失矛纹。
單例對象如果持有Context臂聋,那么很容易引發(fā)內(nèi)存泄漏,此時需要注意傳遞給單例對象的Context最好是 Application Context或南。
類圖
寫法
lazy initialization, thread-unsafety(懶漢法孩等,線程不安全)
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null)
instance = new Singleton();//用到的時候才初始化
return instance;
}
}
需要注意的是這種寫法在多線程操作中是不安全的,后果是可能會產(chǎn)生多個Singleton對象采够,比如兩個線程同時執(zhí)行getInstance()函數(shù)時肄方,然后同時執(zhí)行到 new 操作時,最后很有可能會創(chuàng)建兩個不同的對象蹬癌。
lazy initialization, thread-safety, double-checked(懶漢法权她,線程安全)
需要做到線程安全虹茶,就需要確保任意時刻只能有且僅有一個線程能夠執(zhí)行new Singleton對象的操作,所以可以在getInstance()函數(shù)上加上 synchronized 關鍵字隅要,類似于:
public static synchronized Singleton getInstance() {
if(singleton == null)
instance = new Singleton();
return instance;
}
double-checked(雙重檢測)
但是套用《Head First》上的一句話蝴罪,對于絕大部分不需要同步的情況來說,synchronized 會讓函數(shù)執(zhí)行效率糟糕一百倍以上(Since synchronizing a method could in some extreme cases decrease performance by a factor of 100 or higher)拾徙,所以就有了double-checked(雙重檢測)的方法:
//double-checked(雙重檢測)
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
我們假設兩個線程A,B同時執(zhí)行到了getInstance()這個方法洲炊,第一個if判斷,兩個線程同時為true尼啡,進入if語句暂衡,里面有個 synchronized 同步,所以之后有且僅有一個線程A會執(zhí)行到 synchronized 語句內(nèi)部崖瞭,接著再次判斷instance是否為空狂巢,為空就去new Singleton對象并且賦值給instance,A線程退出 synchronized 語句书聚,交出同步鎖唧领,B線程進入 synchronized 語句內(nèi)部,if判斷instance是否為空雌续,防止創(chuàng)建不同的instance對象斩个,這也是第二個if判斷的作用,B線程發(fā)現(xiàn)不為空驯杜,所以直接退出受啥,所以最終A和B線程可以獲取到同一個Singleton對象,之后的線程調(diào)用getInstance()函數(shù)鸽心,都會因為Instance不為空而直接返回滚局,不會受到 synchronized 的性能影響。
java中volatile關鍵字作用
Java也支持volatile關鍵字顽频,但它被用于其他不同的用途藤肢。當volatile用于一個作用域時,Java保證如下:
(適用于Java所有版本)讀和寫一個volatile變量有全局的排序糯景。也就是說每個線程訪問一個volatile作用域時會在繼續(xù)執(zhí)行之前讀取它的當前值嘁圈,而不是(可能)使用一個緩存的值。(但是并不保證經(jīng)常讀寫volatile作用域時讀和寫的相對順序蟀淮,也就是說通常這并不是有用的線程構建)丑孩。
(適用于Java5及其之后的版本)volatile的讀和寫建立了一個happens-before關系,類似于申請和釋放一個互斥鎖[8]灭贷。
使用volatile會比使用鎖更快温学,但是在一些情況下它不能工作。volatile使用范圍在Java5中得到了擴展甚疟,特別是雙重檢查鎖定現(xiàn)在能夠正確工作[9]仗岖。
上面有一個細節(jié)逃延,java 5版本之后volatile的讀與寫才建立了一個happens-before的關系,之前的版本會出現(xiàn)一個問題:Why is volatile used in this example of double checked locking轧拄,這個答案寫的很清楚了揽祥,線程 A 在完全構造完 instance 對象之前就會給 instance 分配內(nèi)存,線程B在看到 instance 已經(jīng)分配了內(nèi)存不為空就回去使用它檩电,所以這就造成了B線程使用了部分初始化的 instance 對象拄丰,最后就會出問題了。Double-checked locking里面有一句話
As of J2SE 5.0, this problem has been fixed. The volatile keyword now ensures that multiple threads handle the singleton instance correctly. This new idiom is
described in [2] and [3].
eager initialization thread-safety (餓漢法俐末,線程安全)
“餓漢法”就是在使用該變量之前就將該變量進行初始化料按,這當然也就是線程安全的了,寫法也很簡單:
private static Singleton instance = new Singleton();
private Singleton(){
name = "eager initialization thread-safety 1";
}
public static Singleton getInstance(){
return instance;
}
或者
private static Singleton instance = null;
private Singleton(){
name = "eager initialization thread-safety 2";
}
static {
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
代碼都很簡單卓箫,一個是直接進行初始化载矿,另一個是使用靜態(tài)塊進行初始化,目的都是一個:在該類進行加載的時候就會初始化該對象烹卒,而不管是否需要該對象闷盔。這么寫的好處是編寫簡單,而且是線程安全的旅急,但是這時候初始化instance顯然沒有達到lazy loading的效果逢勾。
static inner class thread-safety (靜態(tài)內(nèi)部類,線程安全)
由于在java中藐吮,靜態(tài)內(nèi)部類是在使用中初始化的敏沉,所以可以利用這個天生的延遲加載特性,去實現(xiàn)一個簡單炎码,延遲加載,線程安全的單例模式:
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
private Singleton(){
name = "static inner class thread-safety";
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
定義一個 SingletonHolder 的靜態(tài)內(nèi)部類秋泳,在該類中定義一個外部類 Singleton 的靜態(tài)對象潦闲,并且直接初始化,在外部類 Singleton 的 getInstance() 方法中直接返回該對象迫皱。由于靜態(tài)內(nèi)部類的使用是延遲加載機制歉闰,所以只有當線程調(diào)用到 getInstance() 方法時才會去加載 SingletonHolder 類,加載這個類的時候又會去初始化 instance 變量卓起,所以這個就實現(xiàn)了延遲加載機制和敬,同時也只會初始化這一次,所以也是線程安全的戏阅,寫法也很簡單昼弟。
PS
上面提到的所有實現(xiàn)方式都有兩個共同的缺點:
都需要額外的工作(Serializable、transient奕筐、readResolve())來實現(xiàn)序列化舱痘,否則每次反序列化一個序列化的對象實例時都會創(chuàng)建一個新的實例变骡。
可能會有人使用反射強行調(diào)用我們的私有構造器(如果要避免這種情況,可以修改構造器芭逝,讓它在創(chuàng)建第二個實例的時候拋異常)塌碌。
enum (枚舉寫法)
JDK1.5 之后加入 enum 特性,可以使用 enum 來實現(xiàn)單例模式:
enum SingleEnum{
INSTANCE("enum singleton thread-safety");
private String name;
SingleEnum(String name){
this.name = name;
}
public String getName(){
return name;
}
}
使用枚舉除了線程安全和防止反射強行調(diào)用構造器之外旬盯,還提供了自動序列化機制台妆,防止反序列化的時候創(chuàng)建新的對象。因此胖翰,Effective Java推薦盡可能地使用枚舉來實現(xiàn)單例接剩。但是很不幸的是 android 中并不推薦使用 enum ,主要是因為在 java 中枚舉都是繼承自 java.lang.Enum 類泡态,首次調(diào)用時搂漠,這個類會調(diào)用初始化方法來準備每個枚舉變量。每個枚舉項都會被聲明成一個靜態(tài)變量某弦,并被賦值桐汤。在實際使用時會有點問題,這是 google 的官方文檔介紹:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android
這篇博客也專門計算了 enum 的大邪凶场:胡凱-The price of ENUMs怔毛,所以枚舉寫法的缺點也就很明顯了。
登記式
登記式單例實際上維護了一組單例類的實例腾降,將這些實例存放在一個Map(登記奔鸲取)中,對于已經(jīng)登記過的實例螃壤,則從Map直接返回抗果,對于沒有登記的,則先登記奸晴,然后返回冤馏。
//類似Spring里面的方法,將類名注冊寄啼,下次從里面直接獲取逮光。
public class Singleton {
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
static{
Singleton single = new Singleton(); //構造了一個簡單的實例
map.put(single.getClass().getName(), single);
}
//保護的默認構造子
protected Singleton(){}
//靜態(tài)工廠方法,返還此類惟一的實例
public static Singleton getInstance(String name) {
if(name == null) {
name = Singleton.class.getName();
System.out.println("name == null"+"--->name="+name);
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一個示意性的商業(yè)方法
public String about() {
return "Hello, I am RegSingleton.";
}
public static void main(String[] args) {
Singleton single3 = Singleton.getInstance(null);
System.out.println(single3.about());
}
}
這種方式我極少見到,另外其實內(nèi)部實現(xiàn)還是用的餓漢式單例墩划,因為其中的static方法塊涕刚,它的單例在類被裝載的時候就被實例化了。
總結(jié)
綜上所述乙帮,平時在 android 中使用** double-checked **或者 SingletonHolder 都是可以的杜漠,畢竟 android 早就不使用 JDK5 之前的版本了。由于 android 中的多進程機制,在不同進程中無法創(chuàng)建同一個 instance 變量碑幅,就像 Application 類會初始化兩次一樣戴陡,這點需要注意。
創(chuàng)建型模式 Rules of thumb
有些時候創(chuàng)建型模式是可以重疊使用的沟涨,有一些抽象工廠模式和原型模式都可以使用的場景恤批,這個時候使用任一設計模式都是合理的;在其他情況下裹赴,他們各自作為彼此的補充:抽象工廠模式可能會使用一些原型類來克隆并且返回產(chǎn)品對象喜庞。
抽象工廠模式,建造者模式和原型模式都能使用單例模式來實現(xiàn)他們自己棋返;抽象工廠模式經(jīng)常也是通過工廠方法模式實現(xiàn)的延都,但是他們都能夠使用原型模式來實現(xiàn);
通常情況下睛竣,設計模式剛開始會使用工廠方法模式(結(jié)構清晰晰房,更容易定制化,子類的數(shù)量爆炸)射沟,如果設計者發(fā)現(xiàn)需要更多的靈活性時殊者,就會慢慢地發(fā)展為抽象工廠模式,原型模式或者建造者模式(結(jié)構更加復雜验夯,使用靈活)猖吴;
原型模式并不一定需要繼承,但是它確實需要一個初始化的操作挥转,工廠方法模式一定需要繼承海蔽,但是不一定需要初始化操作;
使用裝飾者模式或者組合模式的情況通常也可以使用原型模式來獲得益處绑谣;
單例模式中党窜,只要將構造方法的訪問權限設置為 private 型,就可以實現(xiàn)單例借宵。但是原型模式的 clone 方法直接無視構造方法的權限來生成新的對象幌衣,所以,單例模式與原型模式是沖突的暇务,在使用時要特別注意。
轉(zhuǎn)載ref: java/android 設計模式學習筆記(1)---單例模式