單例模式深度解析:從餓漢式到枚舉實(shí)現(xiàn)的全方位解讀
單例設(shè)計(jì)模式概念
就是采取一定的方法保證在整個(gè)的軟件系統(tǒng)中,對(duì)某個(gè)類(lèi)只能存在一個(gè)對(duì)象實(shí)例,并且該類(lèi)只提供一個(gè)取得其對(duì)象實(shí)例的方法。如果我們要讓類(lèi)在一個(gè)虛擬機(jī)中只能產(chǎn)生一個(gè)對(duì)象,我們首先必須將類(lèi)的構(gòu)造器的訪問(wèn)權(quán)限設(shè)置為private,這樣,就不能用new操作符在類(lèi)的外部產(chǎn)生類(lèi)的對(duì)象了,但在類(lèi)內(nèi)部仍可以產(chǎn)生該類(lèi)的對(duì)象。因?yàn)樵陬?lèi)的外部開(kāi)始還無(wú)法得到類(lèi)的對(duì)象,只能調(diào)用該類(lèi)的某個(gè)靜態(tài)方法以返回類(lèi)內(nèi)部創(chuàng)建的對(duì)象,靜態(tài)方法只能訪問(wèn)類(lèi)中的靜態(tài)成員變量,所以,指向類(lèi)內(nèi)部產(chǎn)生的該類(lèi)對(duì)象的變量也必須定義成靜態(tài)的。
餓漢式
class Singleton {
// 1.私有化構(gòu)造器
private Singleton() {
}
// 2.內(nèi)部提供一個(gè)當(dāng)前類(lèi)的實(shí)例
// 4.此實(shí)例也必須靜態(tài)化
private static Singleton single = new Singleton();
// 3.提供公共的靜態(tài)的方法,返回當(dāng)前類(lèi)的對(duì)象;在內(nèi)存中自始至終都存在
public static Singleton getInstance() {
return single;
}
}
案例:
public static void main(String[] args) {
User user1 = User.getUser();
System.out.println(user1);
User user2 = User.getUser();
System.out.println(user2);
}
class User{
//1、私有化構(gòu)造器
private User() {
}
//2、內(nèi)部提供一個(gè)當(dāng)前類(lèi)的實(shí)例,此實(shí)例也必須靜態(tài)化
private static User user = new User();
//3、提供公共的靜態(tài)的方法,返回當(dāng)前類(lèi)的對(duì)象;在內(nèi)存中自始至終都存在
public static User getUser() {
return user;
}
}
//結(jié)果是一樣的,即同一個(gè)對(duì)象
com.gupao.singleton.User@6d6f6e28
com.gupao.singleton.User@6d6f6e28
static變量在類(lèi)加載的時(shí)候初始化,此時(shí)不會(huì)涉及到多個(gè)線程對(duì)象訪問(wèn)該對(duì)象的問(wèn)題,虛擬機(jī)保證只會(huì)裝載一次該類(lèi),肯定不會(huì)發(fā)生并發(fā)問(wèn)題,無(wú)需使用synchronized 關(guān)鍵字
存在的問(wèn)題:如果只是加載了本類(lèi),而并不需要調(diào)用getUser,則會(huì)造成資源的浪費(fèi)。
總結(jié):線程安全、非懶加載、效率高,資源浪費(fèi)
懶漢式
延遲對(duì)象的創(chuàng)建
方式1:普通創(chuàng)建
public class Singleton {
//私有構(gòu)造方法
private Singleton() {}
//在成員位置創(chuàng)建該類(lèi)的對(duì)象
private static Singleton instance;
//對(duì)外提供靜態(tài)方法獲取該對(duì)象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果是多線程環(huán)境,以上代碼會(huì)出現(xiàn)線程安全問(wèn)題。
方式2:方法加鎖
class Singleton {
// 1.私有化構(gòu)造器
private Singleton() {
}
// 2.內(nèi)部提供一個(gè)當(dāng)前類(lèi)的實(shí)例
// 4.此實(shí)例也必須靜態(tài)化
private static Singleton instance;
// 3.提供公共的靜態(tài)的方法,返回當(dāng)前類(lèi)的對(duì)象
public static synchronized Singleton getInstance() {//注意多線程情況
if(instance== null) {
instance= new Singleton();
}
return instance;
}
}
以上使用同步方法會(huì)造成每次獲取實(shí)例的線程都要等鎖,會(huì)對(duì)系統(tǒng)性能造成影響,未能完全發(fā)揮系統(tǒng)性能,可使用同步代碼塊來(lái)解決
方式3:雙重檢查鎖
對(duì)于 getInstance() 方法來(lái)說(shuō),絕大部分的操作都是讀操作,讀操作是線程安全的,所以我們沒(méi)必讓每個(gè)線程必須持有鎖才能調(diào)用該方法,我們需要調(diào)整加鎖的時(shí)機(jī)。由此也產(chǎn)生了一種新的實(shí)現(xiàn)模式:雙重檢查鎖模式
public class Singleton {
//私有構(gòu)造方法
private Singleton() {}
private volatile static Singleton instance;
//對(duì)外提供靜態(tài)方法獲取該對(duì)象
public static Singleton getInstance() {
//第一次判斷,如果instance不為null,不進(jìn)入搶鎖階段,直接返回實(shí)例
if(instance == null) { // ①
synchronized (Singleton.class) {
//搶到鎖之后再次判斷是否為null
if(instance == null) {
instance = new Singleton();// ②
}
}
}
return instance;
}
}
為什么判斷兩次instance==null
第一次判斷是在代碼塊前,第二次是進(jìn)入代碼塊后,第二個(gè)判斷想必都知道,多個(gè)線程都堵到代碼塊前等待鎖的釋放,進(jìn)入代碼塊后要獲取到最新的instance值,如果為空就進(jìn)行創(chuàng)建對(duì)象。那么為什么還要進(jìn)行第一個(gè)判斷,第一個(gè)判斷起到優(yōu)化作用,假設(shè)如果instance已經(jīng)不為空了,那么沒(méi)有第一個(gè)判斷仍然會(huì)有線程堵在代碼塊前等待進(jìn)一步判斷,所以如果不為空,有了第一個(gè)判斷就不用再去進(jìn)入代碼塊進(jìn)行判斷,也就不用再去等鎖了,直接返回。
為什么要加volatile?
- 是為了防止指令重排序,給私有變量加 volatile 主要是為了防止第 ② 處執(zhí)行時(shí),也就是“instance = new Singleton()”執(zhí)行時(shí)的指令重排序的,這行代碼看似只是一個(gè)創(chuàng)建對(duì)象的過(guò)程,然而它的實(shí)際執(zhí)行卻分為以下 3 步:
試想一下,如果不加 volatile,那么線程A在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線程 A在執(zhí)行完第 3 步之后,如果來(lái)了線程 B執(zhí)行到上述代碼的第 ① 處,判斷 instance 對(duì)象已經(jīng)不為 null,但此時(shí)線程 A還未將對(duì)象實(shí)例化完,那么線程B將會(huì)得到一個(gè)被實(shí)例化“一半”的對(duì)象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。
- 創(chuàng)建內(nèi)存空間。
- 在內(nèi)存空間中初始化對(duì)象 Singleton。
- 將內(nèi)存地址賦值給 instance 對(duì)象(執(zhí)行了此步驟,instance 就不等于 null 了)。
- 優(yōu)化作用,synchronized塊只有執(zhí)行完才會(huì)同步到主內(nèi)存,那么比如說(shuō)instance剛創(chuàng)建完成,不為空,但還沒(méi)有跳出synchronized塊,此時(shí)又有10000個(gè)線程調(diào)用方法,那么如果沒(méi)有volatile,此使instance在主內(nèi)存中仍然為空,這一萬(wàn)個(gè)線程仍然要通過(guò)第一次判斷,進(jìn)入代碼塊前進(jìn)行等待,正是有了volatile,一旦instance改變,那么便會(huì)同步到主內(nèi)存,即使沒(méi)有出synchronized塊,instance仍然同步到了主內(nèi)存,通過(guò)不了第一個(gè)判斷也就避免了新加的10000個(gè)線程進(jìn)入去爭(zhēng)取鎖。
總結(jié):線程安全、懶加載、效率高。
靜態(tài)內(nèi)部類(lèi)(延遲初始化占位類(lèi))
靜態(tài)內(nèi)部類(lèi)單例模式中實(shí)例由內(nèi)部類(lèi)創(chuàng)建,由于 JVM 在加載外部類(lèi)的過(guò)程中, 是不會(huì)加載靜態(tài)內(nèi)部類(lèi)的, 只有內(nèi)部類(lèi)的屬性/方法被調(diào)用時(shí)才會(huì)被加載, 并初始化其靜態(tài)屬性。靜態(tài)屬性由于被 static 修飾,保證只被實(shí)例化一次,并且嚴(yán)格保證實(shí)例化順序。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder{
private static final Singleton Instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.Instance;
}
}
第一次加載Singleton類(lèi)時(shí)不會(huì)去初始化INSTANCE,只有第一次調(diào)用getInstance,虛擬機(jī)加載SingletonHolder并初始化INSTANCE,這樣不僅能確保線程安全,也能保證 Singleton 類(lèi)的唯一性。
靜態(tài)內(nèi)部類(lèi)單例模式是一種優(yōu)秀的單例模式,是開(kāi)源項(xiàng)目中比較常用的一種單例模式。在沒(méi)有加任何鎖的情況下,保證了多線程下的安全,并且沒(méi)有任何性能影響和空間的浪費(fèi)。
總結(jié):線程安全、懶加載、效率高。
枚舉
枚舉類(lèi)實(shí)現(xiàn)單例模式是極力推薦的單例實(shí)現(xiàn)模式,因?yàn)槊杜e類(lèi)型是線程安全的,并且只會(huì)裝載一次,設(shè)計(jì)者充分的利用了枚舉的這個(gè)特性來(lái)實(shí)現(xiàn)單例模式,枚舉的寫(xiě)法非常簡(jiǎn)單,而且枚舉類(lèi)型是所用單例實(shí)現(xiàn)中唯一一種不會(huì)被破壞的單例實(shí)現(xiàn)模式。
public enum Singleton {
INSTANCE;
}
提供了序列化機(jī)制,保證線程安全,絕對(duì)防止多次實(shí)例化,即使是在面對(duì)復(fù)雜的序列化或者反射攻擊的時(shí)候。
枚舉方式屬于餓漢式方式,會(huì)浪費(fèi)資源
總結(jié):線程安全、非懶加載、效率高。
幾種方式對(duì)比
方式 | 優(yōu)點(diǎn) | 缺點(diǎn) |
餓漢式 | 線程安全、效率高 | 非懶加載,資源浪費(fèi) |
懶漢式synchronized方法 | 線程安全、懶加載 | 效率低 |
懶漢式雙重檢測(cè) | 線程安全、懶加載、效率高 | 無(wú) |
靜態(tài)內(nèi)部類(lèi) | 線程安全、懶加載、效率高 | 無(wú) |
枚舉 | 線程安全、效率高 | 非懶加載,資源浪費(fèi) |