單例設(shè)計模式是最簡單的一種創(chuàng)建型設(shè)計模式,其提供了創(chuàng)建對象的最佳實現(xiàn)。該模式涉及到一個單一的類,且該類負責(zé)創(chuàng)建自己的對象,同時確保只有一個對象被創(chuàng)建。該類提供了一種訪問其唯一對象的方式,訪問方式的實現(xiàn)是單例設(shè)計模式的核心,涉及到線程安全等問題。
- 為何使用單例模式?
通常來說,我們獲取到一個對象實例,當(dāng)只使用其內(nèi)部提供的方法,而不關(guān)注其成員變量時,可以使用單例模式。- 節(jié)省內(nèi)存空間:單例在內(nèi)存中只有一個實例對象,節(jié)省內(nèi)存空間,避免大量創(chuàng)建及銷毀
- 高性能:減少高質(zhì)量資源重復(fù)占用,可以進行全部訪問
單例模式理論
單例模式
- 什么時候使用單例模式?
- 重復(fù)對象需要頻繁實例化及銷毀的對象
- 有狀態(tài)化的工具對象
- 頻繁訪問數(shù)據(jù)庫或文件的對象
單例角色:單例模式只有一個單例角色,在單例的內(nèi)部生成一個對象實例,同時提供一個方法用于獲取這個對象實例。為了避免外界直接創(chuàng)建對象實例從而破壞單例模式,通常構(gòu)造函數(shù)都是設(shè)置為私有的,外部只能通過方法獲取到唯一的單例對象。
單例模式的實現(xiàn)方式
比較常見的單例實現(xiàn)方式有如下:
- 餓漢式
- 懶漢式
- 靜態(tài)內(nèi)部類
代碼實現(xiàn)
- 餓漢式
package singleton.hungry;
/**
* 單例模式:餓漢式實現(xiàn)方式,類在加載的時候就創(chuàng)建單例對象
*/
public class HungrySingleton {
/**
* 定義類靜態(tài)變量,類加載的時候就已經(jīng)創(chuàng)建好單例對象
*/
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
餓漢式在類加載的時候就已經(jīng)將單例對象創(chuàng)建好了,但是如果沒有使用它,一定程度上造成浪費。不過,由于在類加載的時候單例對象就創(chuàng)建好了,因此它是線程安全的。
- 懶漢式
package singleton.lazy;
/**
* 單例模式:懶漢式實現(xiàn)方式
*/
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if (INSTANCE == null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
懶漢式是在外部調(diào)用類的方法獲取單例對象時才會創(chuàng)建單例對象,它不會想餓漢式那樣造成浪費。但是,會導(dǎo)致線程安全問題。在高并發(fā)情況下,可能多個線程都調(diào)用了構(gòu)造方法來創(chuàng)建對象。為了解決這個線程并發(fā)問題,通常需要加鎖。
- 線程安全版懶漢式
package singleton.safeLazy;
/**
* 單例模式:線程安全的懶漢式實現(xiàn)
*/
public class SafeLazySingleton {
/**
* 加volatile關(guān)鍵字,防止重排序
*/
private static volatile SafeLazySingleton INSTANCE = null;
private SafeLazySingleton(){}
public static SafeLazySingleton getInstance(){
if(INSTANCE == null){
synchronized (SafeLazySingleton.class){
// 雙重校驗
if(INSTANCE == null){
INSTANCE = new SafeLazySingleton();
}
}
}
return INSTANCE;
}
}
線程安全版的懶漢式實現(xiàn)要點是:1. volatile關(guān)鍵字 2. synchronized鎖 3. 雙重校驗判空
1. volatile關(guān)鍵字可以防止初始化對象時由于編譯器的作用導(dǎo)致指令重排序
一個對象初始化的步驟包括:
- 給對象實例分配一塊內(nèi)存空間
instance = allocate(SafeLazySingleton.class) - 針對分配好的內(nèi)存空間的對象實例,執(zhí)行構(gòu)造方法,對這個對象實例進行初始化操作,對各個成員變量賦值,執(zhí)行初始化邏輯。
invokeConstructor(instance) - 經(jīng)過上面兩個步驟,一個對象實例完成;此時將
instance指針指向這塊內(nèi)存空間,賦值給我們引用類型的變量,讓它指向SafeLazySingleton對象的內(nèi)存地址。
以上是正常的步驟,然而在編譯器重排序的作用下,可能真正執(zhí)行的指令順序是 1-> 3-> 2。
所以當(dāng)A線程去創(chuàng)建單例對象實例,進行到3步驟,由于重排序,此時的對象是殘缺的;而此時B線程判斷INSTANCE對象為空,于是進行了某些操作,由于是殘缺對象,因此會出錯。
2. 雙重校驗
第二重校驗是避免A、B兩個線程依次進入了同步代碼塊,重復(fù)創(chuàng)建單例對象。
3. synchronized同步鎖
其實synchronized可以直接加在方法上,這樣就不用雙重校驗了,但是這樣的鎖粒度比較大。
- 靜態(tài)內(nèi)部類
package singleton.staticClass;
/**
* 單例模式:靜態(tài)內(nèi)部類
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){}
/**
* 通過靜態(tài)內(nèi)部類加載時創(chuàng)建單例對象
*/
private static class Inner{
final static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
/**
* 第一次調(diào)用此方法時,觸發(fā)靜態(tài)內(nèi)部類加載從而創(chuàng)建單例對象
* @return
*/
public static StaticInnerClassSingleton getInstance(){
return Inner.INSTANCE;
}
}
此方法利用了JVM類加載的線程安全實現(xiàn)單例。