- 一個類只能生成一個對象,所有對象對它的依賴都是相同的。
- 對象的生產是通過
new
關鍵字完成的(當然也有其他的方式,如對象復制,反射等)。 - 使用
new
關鍵字創建對象時,都會根據輸入的參數調用相應的構造函數,如果我們把構造函數設置為private
私有訪問權限時就可以禁止外部創建對象了。
代碼清單
//皇帝類
public class Emperor {
private static final Emperor emperor = new Emperor();
//私有的構造函數
private Emperor(){
}
//創建方法
public static Emperor getInstance(){
return emperor;
}
public void say(){
system.out.println("My is Emperor");
}
}
通過定義一個私有訪問權限的構造函數,避免被其他類
new
出一個對象,而Emperor自己則可以new
一個對象出來,其他類對該類的訪問都可以通過getInstance
獲得同一個對象。
//臣子類
public class Minister {
public static void main(String [] args) {
for(int day=0; day<3 ; day++){
Emperor emperor = Emperor.getInstance();
emperor.say();
}
}
}
單例模式定義
- 確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例。
單例模式的優點
- 由于單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁地創建、銷毀時,而且創建或銷毀時性能又無法優化,單例模式的優勢就非常明顯。
- 由于單例模式只生成一個實例,所以減少了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啟動時直接產生一個單例對象,然后用永久駐留內存的方式來解決。
- 單例模式可以避免對資源的多重占用,例如一個寫文件動作,由于只有一個實例存在內存中,避免對同一個資源文件的同時寫操作。
- 單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。
單例模式的缺點
- 單例模式一般沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑可以實現。單例模式為什么不能增加接口呢?因為接口對單例模式是沒有任何意義的,它要求
自行實例化
,并且提供單一實例、接口或抽象類是不可能被實例化的。當然,在特殊的情況下,單例模式可以實現接口、被繼承等,需要在系統開發中根據環境判斷。 - 單例模式對測試是不利的,在并行開發環境中,如果單例模式沒有完成,是不能進行測試的,沒有接口也不能使用mock的方式虛擬一個對象。
- 單例模式與單一職責原則有沖突。一個類應該只實現一個邏輯,而不關心它是否是單例的,是不是要單例取決于環境,單例模式把
要單例
和業務邏輯融合在一個類中。
使用場景
在一個系統中,要求一個類有且僅有一個對象,如果出現多個對象就會出現不良反應
,可以采用單例模式。
- 要求生成唯一序列號的環境。
- 在整個項目中需要一個共享訪問點或共享數據,例如一個Web頁面上的計數器,可以不用把每次刷新都記錄到數據庫中,使用單例模式保持計數器的值,并確保是線程安全的。
- 創建一個對象需要消耗的資源過多,如要訪問IO和數據庫等資源。
- 需要定義大量的靜態常量和靜態方法的環境,可以采用單例模式。
注意事項
首先,在高并發情況下,請注意單例模式的線程同步問題。之前的例子不會出現產生多個實例的情況,但是下面的例子就需要考慮線程同步。
?public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
該單例模式在低并發的情況下尚不會出現問題,若系統壓力增大,并發量增加時則可能在內存中出現多個實例,破壞了最初預期。為什么會出現這種情況呢?如果一個線程A執行到singleton = new Singleton()
,但還沒有獲得對象(對象初始化是需要時間的),第二個線程B也在執行,執行到if(singleton == null)
判斷,那么線程B獲得判斷條件也是為真,于是內存中就出現兩個對象。
解決線程不安全的方法有很多,可以在getInstance
方法前加synchronized
關鍵字,也可以在getInstance
方法內增加synchronized
來實現。
其次,需要考慮對象的復制情況。在Java中,對象默認是不可以被復制的,若實現了Cloneable
接口,并實現了clone
方法,則可以直接通過對象復制方式創建一個新對象,對象復制是不用調用類的構造函數,因此即使是私有的構造函數,對象仍然可以被復制。在一般情況下,類復制的情況不需要考慮,很少會出現一個單例類會主動要求被復制的情況,解決該問題最好的方法就是單例類不要實現Cloneable
接口。
單例模式的擴展
需要產生固定數量對象的模式就叫做有上限的多例模式,它是單例模式的一種擴展,采用有上限的多例模式,我們可以在設計時決定在內存中有多少個實例,方便系統進行擴展,修正單例可能存在的性能問題,提供系統的響應速度。
//皇帝類
public class Emperor {
private static int maxNumOfEmperor = 2;
private static ArrayList<String> nameList = new ArrayList<String>();
private static ArrayList<Emperor> emperorList = new ArrayList<Emperor>();
private static int countNumOfEmperor = 0;
static {
for(int i=0; i<maxNumOfEmperor; i++){
emperorList.add(new Emperor("皇"+(i+1)+"帝"));
}
}
//私有的構造函數
private Emperor(){
}
private Emperor(String name){
nameList.add(name);
}
//創建方法
public static Emperor getInstance(){
Random random = new Random();
countNumOfEmperor = random.nextInt(maxNumOfEmperor);
return emperorList.get(countNumOfEmperor);
}
public void say(){
system.out.println("My is Emperor");
}
}