一、定義
確定某個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體
二、UML結構圖
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL1kDM1YDO0EjMx0SNwgTO1QDN1EzMyITMwIDMy0SN1MTMxITMvwlMxAjMwIzLcVTNzETMyEzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
- 需要頻繁的執行個體化和銷毀的對象;
- 有狀态的工具類對象
- 頻繁通路資料庫或檔案對象;
- 確定某個類隻有一個對象的場景,比如一個對象需要消耗的資源過多,通路io、資料庫,需要提供全局配置的場景
四、幾種單例模式
1、餓漢式
聲明靜态時已經初始化,在擷取對象之前就初始化
優點:擷取對象的速度快,線程安全(因為虛拟機保證隻會裝載一次,在裝載類的時候是不會發生并發的)
缺點:耗記憶體(若類中有靜态方法,在調用靜态方法的時候類就會被加載,類加載的時候就完成了單例的初始化,拖慢速度)
/**
* 單例模式:餓漢式
* 在類加載的時候就已經完成了初始化,是以類加載較慢,但擷取對象的速度快
* @author Administrator
*
*/
public class EagerSingleton {
//靜态私有成員,已初始化
private static EagerSingleton instance = new EagerSingleton();
//私有構造函數
private EagerSingleton() {
}
//靜态,不用同步(類加載時已初始化,不會有多線程的問題)
public static EagerSingleton getInstance() {
return instance;
}
}
2、懶漢式
synchronized同步鎖: 多線程下保證單例對象唯一性
優點:單例隻有在使用時才被執行個體化,一定程度上節約了資源
缺點:加入synchronized關鍵字,造成不必要的同步開銷。不建議使用。
/**
* 單例模式:懶漢式(線程安全的懶漢式)
* 比較懶,在類加載時,不建立執行個體,是以類加載速度快,但運作時擷取對象的速度慢
* @author Administrator
*
*/
public class LazySingleton {
//靜态私有成員,沒有初始化
private static LazySingleton instance = null;
//私有構造函數
private LazySingleton() {
}
//靜态,同步,公開通路點
public static synchronized LazySingleton getInstace() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
3、Double Check Lock(DCL)實作單例(使用最多的單例實作之一)
雙重鎖定展現在兩次判空
優點:既能保證線程安全,且單例對象初始化後調用getInstance不進行同步鎖,資源使用率高
缺點:第一次加載稍慢,由于Java記憶體模型一些原因偶爾會失敗,在高并發環境下也有一定的缺陷,但機率很小。
/**
* 單例模式:雙重鎖定式
* @author Administrator
*
*/
public class SingletonKerriganD {
//這裡加volatitle是為了避免DCL失效
private volatile static SingletonKerriganD instance = null;
//私有構造函數
private SingletonKerriganD() {
}
/**
* DCL對instance進行了兩次null判斷
* 第一層判斷主要是為了避免不必要的同步
* 第二層判斷則是為了在null的情況下建立執行個體
* @return
*/
public static SingletonKerriganD getInstance() {
if(instance == null) {
synchronized (SingletonKerriganD.class) {
if(instance == null) {
instance = new SingletonKerriganD();
}
}
}
return instance;
}
}
什麼是DCL失效問題?
假如線程A執行到instance = new SingletonKerriganD(),大緻做了如下三件事:
- 給執行個體配置設定記憶體
- 調用構造函數,初始化成員字段
- 将instance 對象指向配置設定的記憶體空間(此時sInstance不是null)
如果執行順序是1-3-2,那多線程下,A線程先執行3,2還沒執行的時候,此時instance!=null,這時候,B線程直接取走instance ,使用會出錯,難以追蹤。JDK1.5及之後的volatile 解決了DCL失效問題(雙重鎖定失效)
4、靜态内部類單例模式
在調用 SingletonHolder.instance 的時候,才會對單例進行初始化
優點:線程安全、保證單例對象唯一性,同時也延遲了單例的執行個體化
缺點:需要兩個類去做到這一點,雖然不會建立靜态内部類的對象,但是其 Class 對象還是會被建立,而且是屬于永久代的對象。
/**
* 單例模式:靜态内部類式
* @author Administrator
*
*/
public class SingletonInner {
//私有構造函數
private SingletonInner() {
}
//在調用SingletonHolder.instance的時候,才會對單例進行初始化
public static class SingletonHolder{
private final static SingletonInner instance = new SingletonInner();
}
public static SingletonInner getInstance() {
return SingletonHolder.instance;
}
}
這種方式如何保證單例且線程安全?
當getInstance方法第一次被調用的時候,它第一次讀取SingletonHolder.instance,内部類SingletonHolder類得到初始化;而這個類在裝載并被初始化的時候,會初始化它的靜态域,進而建立Singleton的執行個體,由于是靜态的域,是以隻會在虛拟機裝載類的時候初始化一次,并由虛拟機來保證它的線程安全性。 這個模式的優勢在于,getInstance方法并沒有被同步,并且隻是執行一個域的通路,是以延遲初始化并沒有增加任何通路成本。
這種方式能否避免反射入侵?
答案是:不能。網上很多介紹到靜态内部類的單例模式的優點會提到“通過反射,是不能從外部類擷取内部類的屬性的。 是以這種形式,很好的避免了反射入侵”,這是錯誤的,反射是可以擷取内部類的屬性(想了解更多反射的知識請看 java反射全解),入侵單例模式根本不在話下
【注意】:上述四種方法要杜絕在被反序列化時重新聲明對象,需要加入如下方法:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}
為什麼呢?因為當JVM從記憶體中反序列化地"組裝"一個新對象時,自動調用 readResolve方法來傳回我們指定好的對象
5、枚舉單例
優點:線程安全,防止被反序列化
缺點:枚舉相對耗記憶體
public enum SingletonEnum {
instance;
public void doThing(){
}
}
隻要 SingletonEnum.INSTANCE 即可獲得所要執行個體。
這種方式如何保證單例?
首先,在枚舉中我們明确了構造方法限制為私有,在我們通路枚舉執行個體時會執行構造方法,同時每個枚舉執行個體都是static final類型的,也就表明隻能被執行個體化一次。在調用構造方法時,我們的單例被執行個體化。 也就是說,因為enum中的執行個體被保證隻會被執行個體化一次,是以我們的INSTANCE也被保證執行個體化一次。
上面示例中生成的位元組碼檔案對instance的描述如下:
...
public static final eft.reflex.SingletonEnum instance;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
...
可以看出,會自動生成 ACC_STATIC, ACC_FINAL這兩個修飾符
枚舉類型為什麼是線程安全的?
我們定義的一個枚舉,在第一次被真正用到的時候,會被虛拟機加載并初始化,而這個初始化過程是線程安全的。而我們知道,解決單例的并發問題,主要解決的就是初始化過程中的線程安全問題。是以,由于枚舉的以上特性,枚舉實作的單例是天生線程安全的。
6、使用容器實作單例模式
在程式的初始化,将多個單例類型注入到一個統一管理的類中,使用時通過key來擷取對應類型的對象,這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統一的接口進行操作。這種方式是利用了Map的key唯一性來保證單例。
import java.util.HashMap;
import java.util.Map;
/**
* 單例模式:容器模式
* @author Administrator
*
*/
public class SingletonManager {
private static Map<String, Object> map = new HashMap<String, Object>();
private SingletonManager() {
}
public static void registerService(String key, Object instance) {
if(!map.containsKey(key)) {
map.put(key, instance);
}
}
public static Object getService(String key) {
return map.get(key);
}
}
五、總結
所有單例模式需要處理得問題都是:
- 将構造函數私有化
- 通過靜态方法擷取一個唯一執行個體
- 保證線程安全
- 防止反序列化造成的新執行個體等。
推薦使用:DCL、靜态内部類、枚舉
單例模式優點
- 隻有一個對象,記憶體開支少、性能好(當一個對象的産生需要比較多的資源,如讀取配置、産生其他依賴對象時,可以通過應用啟動時直接産生一個單例對象,讓其永駐記憶體的方式解決)
- 避免對資源的多重占用(一個寫檔案操作,隻有一個執行個體存在記憶體中,避免對同一個資源檔案同時寫操作)
- 在系統設定全局通路點,優化和共享資源通路(如:設計一個單例類,負責所有資料表的映射處理)
單例模式缺點
- 一般沒有接口,擴充難
- android中,單例對象持有Context容易記憶體洩露,此時需要注意傳給單例對象的Context最好是Application Context