概念
java中單例模式是一種常見的設(shè)計(jì)模式窍箍,單例模式分三種:懶漢式單例串纺、餓漢式單例丽旅、登記式單例三
種。
特點(diǎn)
單例模式有以下特點(diǎn):
- 單例類只能有一個(gè)實(shí)例纺棺。
- 單例類必須自己創(chuàng)建自己的唯一實(shí)例榄笙。
- 單例類必須給所有其他對(duì)象提供這一實(shí)例
單例模式確保某個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例祷蝌。避免生成多個(gè)對(duì)
象保證只對(duì)這一個(gè)唯一對(duì)象進(jìn)行操作茅撞,保證線程的安全和數(shù)據(jù)的安全.
餓漢式:
顧名思義,餓漢式就是在第一次引用該類的時(shí)候就創(chuàng)建對(duì)象實(shí)例巨朦,而不管實(shí)際是否需要?jiǎng)?chuàng)建米丘。代碼如
下:
下面看一個(gè)示例:
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){ return singleton; }
}
這樣做的好處是編寫簡單,但是無法做到延遲創(chuàng)建對(duì)象罪郊。但是我們很多時(shí)候都希望對(duì)象可以盡可能地
延遲加載蠕蚜,從而減小負(fù)載尚洽,所以就需要下面的懶漢式
懶漢式:
單線程寫法
public class Singleton { private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; }}
這種寫法是最簡單的悔橄,由私有構(gòu)造器和一個(gè)公有靜態(tài)工廠方法構(gòu)成,在工廠方法中對(duì)singleton進(jìn)行null
判斷腺毫,如果是null就new一個(gè)出來癣疟,最后返回singleton對(duì)象。這種方法可以實(shí)現(xiàn)延時(shí)加載潮酒,但是有一個(gè)
致命弱點(diǎn):線程不安全睛挚。如果有兩條線程同時(shí)調(diào)用getSingleton()方法,就有很大可能導(dǎo)致重復(fù)創(chuàng)建對(duì)
象急黎。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}
考慮線程安全的寫法:
這種寫法考慮了線程安全扎狱,將對(duì)singleton的null判斷以及new的部分使用synchronized進(jìn)行加鎖。同
時(shí)勃教,對(duì)singleton對(duì)象使用volatile關(guān)鍵字進(jìn)行限制淤击,保證其對(duì)所有線程的可見性,并且禁止對(duì)其進(jìn)行指
令重排序優(yōu)化故源。如此即可從語義上保證這種單例模式寫法是線程安全的污抬。注意,這里說的是語義上绳军,實(shí)
際使用中還是存在小坑的
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
兼顧線程安全和效率的寫法:
雖然上面這種寫法是可以正確運(yùn)行的印机,但是其效率低下,還是無法實(shí)際應(yīng)用门驾。因?yàn)槊看握{(diào)用
getSingleton()方法射赛,都必須在synchronized這里進(jìn)行排隊(duì),而真正遇到需要new的情況是非常少的奶是。
所以楣责,就誕生了第三種寫法
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
這種寫法被稱為“雙重檢查鎖”顷蟆,顧名思義,就是在getSingleton()方法中腐魂,進(jìn)行兩次null檢查帐偎。看似多
此一舉蛔屹,但實(shí)際上卻極大提升了并發(fā)度削樊,進(jìn)而提升了性能。為什么可以提高并發(fā)度呢兔毒?就像上文說的漫贞,
在單例中new的情況非常少,絕大多數(shù)都是可以并行的讀操作育叁。因此在加鎖前多進(jìn)行一次null檢查就可
以減少絕大多數(shù)的加鎖操作迅脐,執(zhí)行效率提高的目的也就達(dá)到了
小坑:
那么,這種寫法是不是絕對(duì)安全呢豪嗽?前面說了谴蔑,從語義角度來看,并沒有什么問題龟梦。但是其實(shí)還是有
坑隐锭。說這個(gè)坑之前我們要先來看看 volatile 這個(gè)關(guān)鍵字。其實(shí)這個(gè)關(guān)鍵字有兩層語義计贰。第一層語義相信
大家都比較熟悉钦睡,就是可見性≡甑梗可見性指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存( Work
Memory )寫回主內(nèi)存( Main Memory )荞怒,所以會(huì)馬上反應(yīng)在其它線程的讀取操作中。順便一提秧秉,工
作內(nèi)存和主內(nèi)存可以近似理解為實(shí)際電腦中的高速緩存和主存褐桌,工作內(nèi)存是線程獨(dú)享的,主存是線程共
享的福贞。 volatile 的第二層語義是禁止指令重排序優(yōu)化撩嚼。大家知道我們寫的代碼(尤其是多線程代碼),
由于編譯器優(yōu)化挖帘,在實(shí)際執(zhí)行的時(shí)候可能與我們編寫的順序不同完丽。編譯器只保證程序執(zhí)行結(jié)果與源代碼
相同,卻不保證實(shí)際指令的順序與源代碼相同拇舀。這在單線程看起來沒什么問題逻族,然而一旦引入多線程,
這種亂序就可能導(dǎo)致嚴(yán)重問題骄崩。 volatile 關(guān)鍵字就可以從語義上解決這個(gè)問題聘鳞。
注意薄辅,前面反復(fù)提到 “ 從語義上講是沒有問題的 ” ,但是很不幸抠璃,禁止指令重排優(yōu)化這條語義直到 jdk1.5
以后才能正確工作站楚。此前的 JDK 中即使將變量聲明為 volatile 也無法完全避免重排序所導(dǎo)致的問題。所
以搏嗡,在 jdk1.5 版本前窿春,雙重檢查鎖形式的單例模式是無法保證線程安全的。
靜態(tài)內(nèi)部類法:
那么采盒,有沒有一種延時(shí)加載旧乞,并且能保證線程安全的簡單寫法呢?我們可以把 Singleton 實(shí)例放到一個(gè)
靜態(tài)內(nèi)部類中磅氨,這樣就避免了靜態(tài)實(shí)例在 Singleton 類加載的時(shí)候就創(chuàng)建對(duì)象尺栖,并且由于靜態(tài)內(nèi)部類只
會(huì)被加載一次,所以這種寫法也是線程安全的:
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
但是烦租,上面提到的所有實(shí)現(xiàn)方式都有兩個(gè)共同的缺點(diǎn):
都需要額外的工作(Serializable延赌、transient、readResolve())來實(shí)現(xiàn)序列化左权,否則每次反序列化一 個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例皮胡。
可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器赏迟,讓它在 創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。
登記式:
登記式單例實(shí)際上維護(hù)了一組單例類的實(shí)例蠢棱,將這些實(shí)例存放在一個(gè) Map (登記毙可薄)中,對(duì)于已經(jīng)登記
過的實(shí)例泻仙,則從 Map 直接返回糕再,對(duì)于沒有登記的,則先登記玉转,然后返回突想。
public class Singleton {
private static Map<String, Singleton> map = new HashMap<String, Singleton> ();
static {
Singleton single = new Singleton();
map.put(single.getClass().getName(), single);
}
// 保護(hù)的默認(rèn)構(gòu)造子 protected Singleton() { }
// 靜態(tài)工廠方法,返還此類惟一的實(shí)例
public static Singleton getInstance(String name) {
if (name == null) {
name = Singleton.class.getName();
System.out.println("name == null" + "--->name=" + name);
}
if (map.get(name) == null) {
try {
map.put(name, (Singleton) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
}
它用的比較少,另外其實(shí)內(nèi)部實(shí)現(xiàn)還是用的餓漢式單例究抓,因?yàn)槠渲械?static 方法塊猾担,它的單例在類被裝
載的時(shí)候就被實(shí)例化了。
枚舉寫法:
當(dāng)然刺下,還有一種更加優(yōu)雅的方法來實(shí)現(xiàn)單例模式绑嘹,那就是枚舉寫法
public enum Singleton {
INSTANCE;
private String name;
public String getName(){ return name; }
public void setName(String name){ this.name = name; }
}
使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制橘茉,防止反序列化
的時(shí)候創(chuàng)建新的對(duì)象工腋。因此推薦盡可能地使用枚舉來實(shí)現(xiàn)單例姨丈。
最后,不管采取何種方案擅腰,請(qǐng)時(shí)刻牢記單例的三大要點(diǎn):
線程安全
延遲加載
序列化與反序列化安全
最后
在文章的最后作者為大家整理了很多資料蟋恬!包括java核心知識(shí)點(diǎn)+全套架構(gòu)師學(xué)習(xí)資料和視頻+一線大廠面試寶典+面試簡歷模板+阿里美團(tuán)網(wǎng)易騰訊小米愛奇藝快手嗶哩嗶哩面試題+Spring源碼合集+Java架構(gòu)實(shí)戰(zhàn)電子書等等!
全部免費(fèi)分享給大家趁冈,有需要的朋友關(guān)注公眾號(hào):【前程有光】自冉钕帧!