動機
有些情況下,一個類只能有一個實例是很重要的仓坞。比如說颜说,在操作系統(tǒng)中只能有一個窗口管理器的(文件系統(tǒng)或打印機程序)购岗。通常, 單實例用于對內(nèi)部或外部資源的集中式管理门粪,同時它們提供一個訪問其自身的全局入口喊积。
單例模式是最簡單的設計模式之一。它只涉及到一個負責實例化它自己的類玄妈,這個類保證其只創(chuàng)建一個實例(私有化構(gòu)造函數(shù))乾吻;同時該類提供一個訪問該實例的全局入口。這樣拟蜻,程序各處都使用同一實例绎签,不會每次都直接調(diào)用構(gòu)造函數(shù)。
目的
- 確保一個類只創(chuàng)建一個實例
- 提供訪問該單一實例的全局入口
實現(xiàn)
具體實現(xiàn)涉及 Singleton 類的一個靜態(tài)私有成員酝锅,一個私有構(gòu)造函數(shù)和一個共有方法返回該靜態(tài)私有成員的引用诡必。
單例模式定義一個 getInstance 方法來暴露供客戶端訪問的單一實例。getInstance() 負責在單一實例還沒被創(chuàng)建的時候創(chuàng)建它并返回該實例搔扁。
class Singleton{
private static Singleton instance;
private Singleton(){
// ...
}
public static synchronized Singleton getInstance(){
if(instance == null)
instance = new Singleton();
return instance;
}
public void doSomething(){
//...
}
}
你可以發(fā)現(xiàn)上面的代碼 getInstance 方法確保只創(chuàng)建一個類實例爸舒。不能從類的外部訪問構(gòu)造函數(shù)以保證只能通過 getIntance 方法來創(chuàng)建類實例。
getInstance 方法也作為對象唯一的全局訪問入口稿蹲,可以像下面這樣使用:
Singleton.getInstance().doSomething();
適用場景 & 例子
根據(jù)定義扭勉,單例模式的使用場景應該是一個類必須只有一個實例,并且必須從一個全局入口訪問這個實例苛聘。以下幾個是使用單例模式的真實案例:
- 日志類 Logger Classes
單例模式被用于日志類的設計中涂炎。 這些日志類通常以單例來實現(xiàn),并且在應用各組件中提供一個全局的日志記錄入口设哗,執(zhí)行日志記錄操作時就不用每次都創(chuàng)建對象了璧尸。 - 配置類 Configuration Classes
使用單例模式設計為應用提供配置的類。通過將配置類實現(xiàn)為單例熬拒,不單單提供全局訪問入口爷光,我們還可以將這個實例作為緩存對象。當實例化類的時候(讀取值)澎粟,單例會將值保持在其內(nèi)部結(jié)構(gòu)中蛀序。如果配置是從數(shù)據(jù)庫或者文件中讀取,這樣就不用每次使用配置參數(shù)時都要去重新載入值了活烙。 - 共享地訪問資源
單例模式可以用于設計需要串行運行的應用徐裸。假設應用中有許多在多線程環(huán)境中運行的類,這些類需要串行地執(zhí)行操作啸盏。在這種情況下重贺, 帶有 synchronized 方法的單例實例就可以用來管理這些串行操作。 - 單例實現(xiàn)的工廠
假設我們設計一個執(zhí)行于多線程環(huán)境下的應用,其中有一個用于生成帶有id的新對象(賬戶气笙,客戶次企,網(wǎng)站,地址等對象)潜圃。如果這工廠類在2個不同的線程中實例化2次缸棵,那么就可能出現(xiàn)id重疊的2個不同對象。如果我們將這個 Factory 實現(xiàn)為一個單例就可以避免這個問題谭期。通常將 抽象工廠 或 工廠方法 同 單例模式 一起使用堵第。
特定的問題和實現(xiàn)
為了在多線程下使用,線程安全的實現(xiàn)
一個健壯的單例實現(xiàn)應該在任何情況下都能正常工作隧出。這就是為什么我們要確保多線程使用時它也能正常工作的原因踏志。如前面例子的單例確保讀寫操作都是同步的,它可用于多線程應用中胀瞪。
一狰贯、 使用雙重鎖定機制實現(xiàn)延遲初始化(懶漢)
上面代碼中展示的標準實現(xiàn)是一種線程安全的實現(xiàn),但它不是最好的線程安全實現(xiàn)赏廓,因為當我們考慮性能時涵紊,同步操作的開銷比較大。我們能看出同步的 getInstance 在實例已經(jīng)創(chuàng)建后并不需要再進行同步幔摸。如果我們發(fā)現(xiàn)實例已經(jīng)創(chuàng)建摸柄,我們只需返回這個實例,而不需要使用任何同步代碼塊既忆。這個優(yōu)化在于在非同步代碼塊中檢查實例是否為 null驱负, 再在同步代碼塊中檢驗是否 null 并且創(chuàng)建實例。這稱為雙重鎖定機制患雇。
在這種情況下跃脊,單例實例在第一次調(diào)用 getInstance() 方法的時候創(chuàng)建。這就叫延遲初始化苛吱,并且它確保這個單例的實例只在需要的時候創(chuàng)建酪术。
// 使用雙重鎖定機制的延遲初始化
class Singleton{
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
// ...
instance = new Singleton();
}
}
}
return instance;
}
public void doSomething(){
// ...
}
}
關(guān)于為什么要加 volatile 可以參考下 https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
二、 使用靜態(tài)字段實現(xiàn)預先加載 (餓漢)
由于以下實現(xiàn)中單例實例被聲明為靜態(tài)成員了翠储,在類加載的時候就實例化了而不是第一次使用它的時候绘雁。這就是為什么我們不再需要同步代碼了。 類只加載一次保證實例的唯一性援所。
class Singleton{
private static Singleton instance = new Singleton();
private Singleton() {
//...
}
public static Singleton getInstance(){
return instance;
}
public void doSomething(){
//...
}
}
protected constructor
可以使用 protected 訪問修飾符的構(gòu)造函數(shù)來授權(quán)給子類庐舟。但是這種技術(shù)有2個缺陷,使得單例的繼承不切實際:
- 首先住拭,如果構(gòu)造函數(shù)是 protected挪略, 意味著這個類可以被同一個包內(nèi)的其他類實例化历帚。可能的措施是隔離單例類杠娱。
- 其次挽牢,要使用派生類,所有的 getInstance 調(diào)用都得從現(xiàn)有代碼中的 Singleton.getInstance() 改為 NewSingleton.getInstance()
如果多個 classloader 訪問同一個單例類墨辛,會有多個單例實例
如果一個類(相同類名,相同包名)被2個不同的 classloader 加載趴俘,那么他們代表內(nèi)存中2個不同的類睹簇。
序列化
如果單例類實現(xiàn)了 java.io.Serializable 接口,當單例實例被序列化和反序列化多次時寥闪,就會創(chuàng)建多個單例類實例太惠。為了避免這種情況,必須實現(xiàn) readResolve 方法疲憋。 參考下 Serializable () 和 readResolve 方法的說明凿渊。
將 抽象工廠 和 工廠方法 實現(xiàn)為單例
在一些特定的場景下工廠必須是唯一的。存在2個工廠的話缚柳,創(chuàng)建對象時會有意料之外的影響埃脏。為了確保工廠的唯一性,它要實現(xiàn)成單例秋忙。這樣做之后我們也避免了使用前的工廠實例化彩掐。
Hot Spot:
- 多線程: 當單例運行于多線程應用時,必須格外小心
- 序列化: 當單例類實現(xiàn)了 Serializable 接口灰追,它們必須實現(xiàn) readResolve 方法以避免 2 個不同的對象
- Classloaders 如果單例類被2個不同的類加載器加載堵幽,我們將得到2個不同類,一個類加載器一個
- 由類名表示的全局訪問入口:單例類的實例通過類名來獲取弹澎。乍一看朴下,這樣很容易訪問實例,但這不是很靈活苦蒿。如果我們要替換這個單例類殴胧,就要修改代碼中所有的引用。
jdk 中的使用
**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
// ...
}
more
示例代碼:https://github.com/minorpoet/design-patterns/tree/master/Singleton
classloader: http://ifeve.com/classloader/
volatile: http://www.reibang.com/p/3893fb35240f
double-check-lock is broken: http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html