單利模式是應用最廣的模式之一,也可能是很多初級工程師唯一會使用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個全局對象,這樣有利於我們協調系統整體的行為。如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoader中又含有線程池、緩存系統、網絡請求等,很消耗資源,因此,沒有理由讓他構造多個實例。
這種不能自由構造對象的情況,就是單利模式的使用場景
確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。
確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。
例如,創建一個對象需要消耗的資源過多,如果要訪問IO和數據庫等資源,這時就要考慮使用單例模式了
角色介紹:
(1)Client——-高層客戶端;
(2)Singleton——單例類
實現單例模式有如下幾個關鍵點:
(1)構造函數不對外開放,一般為Private;
(2)通過一個靜態方法或者枚舉返回單例類對象
(3)確保單例類的對象有且只有一個,尤其是在多線程環境下;
(4)確保單例類對象在反序列化時不會重新構建對象。
通過將單例類的構造函數私有化,使得客戶端代碼不能通過new的形式手動構造單例類的對象。單例類會暴露一個共有靜態方法,客戶端需要調用這個靜態方法獲取到單例類的唯一對象,在或許這個單例對象的過程中需要確保線程安全,及在多線程環境下構造單例類的對象也是有且只有一個,這也是單例模式實現中比較困難的地方。
public class Staff{
public void work(){
//干活
}
}
public class VP extends Staff{
@Overrride
public void work{
//管下面的經理
}
}
public static class CEO extends Staff{
private static final CEO mCeo = new CEO;
//構造函數
private CEO(){
}
//公用的靜態函數,對外暴露獲取單例對象的接口
public static CEO getCEO(){
return mCeo;
}
@Override
public void work(){
//管理VP
}
}
public class Company{
private List allStaffs = new ArrayList();
public void addStaff(Staff per){
allStaffs.add(per);
}
public void showAllStaffs(){
for(Staff per : allStaffs){
System.out.println("Obj"+per.toString());
}
}
}
public class Test{
public static void main(String[] args){
Company cp = new Company();
Staff ceo1 = CEO.getCEO();
Staff ceo2 = CEO.getCEO();
cp.addStaff(ceo1);
cp.addStaff(ceo2);
Staff vp1 = new VP();
Staff vp2 = new VP();
Staff staff1 = new Staff();
Staff staff2 = new Staff();
Staff staff3 = new Staff();
cp.addStaff(vp1);
cp.addStaff(vp2);
cp.addStaff(staff1);
cp.addStaff(staff2);
cp.addStaff(staff3);
cp.showAllStaffs();
}
}
輸出結果可以看到CEO類不能通過new的形式構造對象,只能通過CEO.getCEO()函數來獲取,而這個CEO對象是靜態對象,並且在生命的時候就已經初始化,這就保證了CEO對象的唯一性。從輸出結果中發現,CEO兩次輸出的CEO對象都是一樣的,而VP,Staff等類型的都是不同的,這個實現的核心在於將CEO累的構造方法私有化,使得外部程序不能通過構造函數來構造CEO對象,而CEO類通過一個靜態方法返回一個靜態對象。
懶漢模式是聲明一個靜態對象,並且在用戶第一次調用getInstance時進行初始化,而上述的餓漢模式(CEO類)是在聲明靜態對象時已經初始化。
懶漢單例模式實現如下:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Sigleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
getInstance()加了一個synchronized關鍵字,也就是說getInstance()是一個同步方法,這就是上面所說的在多線程情況下保證單例對象唯一性的手段。
這時就有問題了,即使instance已經被初始化(第一次調用時就會被初始化instance),每次調用getInstance都會進行同步,這樣會消耗不必要的資源,這也就是懶漢單例模式的問題。
最後總結一下,懶漢單例模式的優點是單裡只有在使用時才會被實例化,在一定程度上節約資源。
缺點是第一次加載時需要及時進行實例化,反應稍慢,最大的問題是每次調用getInstance都進行同步,造成不必要的同步開銷。
這種模式一般不是很建議使用。
DCL方式實現單例模式的優點是既能夠在需要時才初始化單例模式,又能保證線程安全,且單例對象初始化後調用getInstance不進行同步鎖。
代碼如下所示:
public class Singleton{
private static Singleton sInstance = null;
private Singleton(){
}
public void doSometing(){
System.out.println("do sth.");
}
public static Singleton getInstance(){
if(mInstahce == null){
synchronized(Singleton.class){
if(mInstance == null){
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
本程序的亮點自然都在getInstance方法上,可以看到getInstance方法對instance進行了兩次判斷:第一層判斷主要是為了避免不必要的同步,第二層判斷則是為了在null的情況下創建實例,這是什麼意思呢?
首先我們要知道sInstance = new Singleton()這個語句,它大致做了三件事情:
(1)給Singleton的實例分配內存;
(2)調用Singleton()的構造函數,初始化成員字段;
(3)將sInstance對象指向分配的內存空間(此時sInstance就不是null了)。
在JDK1.5之前的JMM長Cache、寄存器到主內存回寫順序的規定,上面的第二和第三順序是無法保證的,也就是說,執行順序可能是1-2-3也可能是1-3-2。如果是後者,並且在3執行完畢,2未執行之前,被切換到線程B上,這時候sInstance因為已經在線程A內執行過了第三點,sInstance已經是非空了,所以,線程B直接取走sInstance,再使用時就會出錯,這就是DCL失效的問題,而且這種難以跟蹤。
JDK1.5之後SUN公司調整了JMM,具體化了volatile關鍵字,因此,只需要將sInstance的定義改成private volatile static Singleton sInstance = null就可以保證sInstance對象每次都是從主內存中讀取,就可以使用DCL的寫法來完成單例模式,當然使用volatile或多或少也會影響到性能。
DCL的有點:資源利用率高,第一次執行getInstance是時單例對象才會被實例化,效率高。
缺點:第一次加載時反應有點慢,也由於java的內存模型的原因偶爾會失敗,在高並發環境下也有一定的缺陷,雖然發生的概率很小。DCL模式是使用最多的單例實現方式,它能夠在需要時才實例化單例對象,並且能夠在絕大多數場景下保證單例對象的唯一性。除非代碼在並發場景比較復雜。
DCL雖然在一定程度上解決了資源消耗、多余的同步、線程安全等問題但是,他還是在某下情況下出現時效的問題,這個問題被稱為雙重檢查鎖定失效,在《java並發編程實踐》這一書中有討論。
建議使用如下代碼進行替換
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,只有在第一次調用Singleton的getInstance方法時才會導致sInstance被初始化。因此,第一次調用getInstance方法會導致虛擬機加載SingletonHolder類,這種方式不僅能夠保證線程安全,也能夠保證單例對象的唯一性,同事也延遲了單例的實例化,所以這是推薦使用單例模式的實現方式。
枚舉就是最簡單的單例實現方法
public enum SingleEnum{
INSTANCE;
public void doSomething{
System.out.println("do sth.");
}
}
寫法簡單就是枚舉最大的優點,枚舉在Java中與普通的類是一樣的,不僅能夠有字段,還能夠有自己的方法。更重要的是默認枚舉實例的創建是線程安全的,並且在任何情況下它都是一個單例。
public class SingletonManager{
private static Map objMap = new HashMap();
private Singleton(){}
public static void registerService(String key,Object instance){
if (!objMap.containKey(key)) {
objMap.put(key,instance);
}
}
public static ObjectgetService(String key){
return objMap.get(key);
}
}
這種形式可以將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本也對用戶隱藏了具體實現,降低了耦合度。
總而言之,不管以那種形式實現單例模式,他的核心原理都是將構造函數私有化,並且通過靜態方法獲取一個唯一的實例,在這個獲取的過程中必須保證線程安全、防止反序列化導致重新生成實例對象等問題。