單例模式的定義
單例模式就是在程序運(yùn)行中只實(shí)例化一次重归,創(chuàng)建一個(gè)全局唯一對象桅狠,有點(diǎn)像 Java 的靜態(tài)變量奸笤,但是單例模式要優(yōu)于靜態(tài)變量痕檬,靜態(tài)變量在程序啟動的時(shí)候JVM就會進(jìn)行加載冤寿,如果不使用歹苦,會造成大量的資源浪費(fèi),單例模式能夠?qū)崿F(xiàn)懶加載督怜,能夠在使用實(shí)例的時(shí)候才去創(chuàng)建實(shí)例殴瘦。開發(fā)工具類庫中的很多工具類都應(yīng)用了單例模式,比如線程池号杠、緩存蚪腋、日志對象等,它們都只需要?jiǎng)?chuàng)建一個(gè)對象究流,如果創(chuàng)建多份實(shí)例辣吃,可能會帶來不可預(yù)知的問題,比如資源的浪費(fèi)芬探、結(jié)果處理不一致等問題神得。
單例的實(shí)現(xiàn)思路
靜態(tài)化實(shí)例對象
私有化構(gòu)造方法,禁止通過構(gòu)造方法創(chuàng)建實(shí)例
提供一個(gè)公共的靜態(tài)方法偷仿,用來返回唯一實(shí)例
單例的好處
只有一個(gè)對象哩簿,內(nèi)存開支少、性能好
避免對資源的多重占用
在系統(tǒng)設(shè)置全局訪問點(diǎn)酝静,優(yōu)化和共享資源訪問
單例模式的實(shí)現(xiàn)
單例模式的寫法有餓漢模式节榜、懶漢模式、雙重檢查鎖模式别智、靜態(tài)內(nèi)部類單例模式宗苍、枚舉類實(shí)現(xiàn)單例模式五種方式,其中懶漢模式、雙重檢查鎖模式讳窟,如果你寫法不當(dāng)让歼,在多線程情況下會存在不是單例或者單例出異常等問題,具體的原因丽啡,在后面的對應(yīng)處會進(jìn)行說明谋右。我們從最基本的餓漢模式開始我們的單例編寫之路。
1. 餓漢模式
餓漢模式采用一種簡單粗暴的形式补箍,在定義靜態(tài)屬性時(shí)改执,直接實(shí)例化了對象。代碼如下:
//在類加載時(shí)就完成了初始化坑雅,所以類加載較慢辈挂,但獲取對象的速度快
public class SingletonObject1 {
// 利用靜態(tài)變量來存儲唯一實(shí)例
private static final SingletonObject1 instance = new SingletonObject1();
// 私有化構(gòu)造函數(shù)
private SingletonObject1(){
// 里面可能有很多操作
}
// 提供公開獲取實(shí)例接口
public static SingletonObject1 getInstance(){
return instance;
}
}
餓漢模式的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn)
由于使用了static關(guān)鍵字,保證了在引用這個(gè)變量時(shí)霞丧,關(guān)于這個(gè)變量的所有寫入操作都完成呢岗,所以保證了JVM層面的線程安全 - 缺點(diǎn)
不能實(shí)現(xiàn)懶加載,造成空間浪費(fèi)蛹尝,如果一個(gè)類比較大后豫,我們在初始化的時(shí)就加載了這個(gè)類,但是我們長時(shí)間沒有使用這個(gè)類突那,這就導(dǎo)致了內(nèi)存空間的浪費(fèi)挫酿。
2.懶漢模式
懶漢模式是一種偷懶的模式,在程序初始化時(shí)不會創(chuàng)建實(shí)例愕难,只有在使用實(shí)例的時(shí)候才會創(chuàng)建實(shí)例早龟,所以懶漢模式解決了餓漢模式帶來的空間浪費(fèi)問題,同時(shí)也引入了其他的問題猫缭,我們先來看看下面這個(gè)懶漢模式
public class SingletonObject2 {
// 定義靜態(tài)變量時(shí)葱弟,未初始化實(shí)例
private static SingletonObject2 instance;
// 私有化構(gòu)造函數(shù)
private SingletonObject2(){}
public static SingletonObject2 getInstance(){
// 使用時(shí),先判斷實(shí)例是否為空猜丹,如果實(shí)例為空芝加,則實(shí)例化對象
if (instance == null)
instance = new SingletonObject2();
return instance;
}
}
上面是懶漢模式的實(shí)現(xiàn)方式,但是上面這段代碼在多線程的情況下是不安全的射窒,因?yàn)樗荒鼙WC是單例模式藏杖,有可能會出現(xiàn)多份實(shí)例的情況,出現(xiàn)多份實(shí)例的情況是在創(chuàng)建實(shí)例對象時(shí)候造成的脉顿。所以我單獨(dú)把實(shí)例化的代碼提出蝌麸,來分析一下為什么會出現(xiàn)多份實(shí)例的情況。
1 if (instance == null)
2 instance = new SingletonObject2();
假設(shè)有兩個(gè)線程都進(jìn)入到 1 這個(gè)位置艾疟,因?yàn)闆]有任何資源保護(hù)措施来吩,所以兩個(gè)線程可以同時(shí)判斷的instance都為空敢辩,都將去執(zhí)行 2 的實(shí)例化代碼,所以就會出現(xiàn)多份實(shí)例的情況弟疆。
通過上面的分析我們已經(jīng)知道出現(xiàn)多份實(shí)例的原因责鳍,如果我們在創(chuàng)建實(shí)例的時(shí)候進(jìn)行資源保護(hù),是不是可以解決多份實(shí)例的問題兽间?確實(shí)如此,我們給getInstance()方法加上synchronized關(guān)鍵字正塌,使得getInstance()方法成為受保護(hù)的資源就能夠解決多份實(shí)例的問題嘀略。加上synchronized關(guān)鍵字之后代碼如下:
public class SingletonObject3 {
private static SingletonObject3 instance;
private SingletonObject3(){}
public synchronized static SingletonObject3 getInstance(){
/**
* 添加class類鎖,影響了性能乓诽,加鎖之后將代碼進(jìn)行了串行化帜羊,
* 我們的代碼塊絕大部分是讀操作,在讀操作的情況下鸠天,代碼線程是安全的
*/
if (instance == null)
instance = new SingletonObject3();
return instance;
}
}
經(jīng)過修改后讼育,解決了多份實(shí)例的問題,但是因?yàn)橐雜ynchronized關(guān)鍵字稠集,對代碼加了鎖奶段,就引入了新的問題,加鎖之后會使得程序變成串行化剥纷,只有搶到鎖的線程才能去執(zhí)行這段代碼塊痹籍,這會使得系統(tǒng)的性能大大下降。
懶漢模式的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn)
實(shí)現(xiàn)了懶加載晦鞋,節(jié)約了內(nèi)存空間 - 缺點(diǎn)
在不加鎖的情況下蹲缠,線程不安全,可能出現(xiàn)多份實(shí)例
在加鎖的情況下悠垛,會是程序串行化线定,使系統(tǒng)有嚴(yán)重的性能問題
3. 雙重檢查鎖模式
再來討論一下懶漢模式中加鎖的問題,對于getInstance()方法來說确买,絕大部分的操作都是讀操作斤讥,讀操作是線程安全的,所以我們沒必讓每個(gè)線程必須持有鎖才能調(diào)用該方法拇惋,我們需要調(diào)整加鎖的問題周偎。由此也產(chǎn)生了一種新的實(shí)現(xiàn)模式:雙重檢查鎖模式,下面是雙重檢查鎖模式的單例實(shí)現(xiàn)代碼塊:
public class SingletonObject4 {
private static SingletonObject4 instance;
private SingletonObject4(){
}
public static SingletonObject4 getInstance(){
// 第一次判斷撑帖,如果這里為空蓉坎,不進(jìn)入搶鎖階段,直接返回實(shí)例
if (instance == null)
synchronized (SingletonObject4.class){
// 搶到鎖之后再次判斷是否為空
if (instance == null){
instance = new SingletonObject4();
}
}
return instance;
}
}
雙重檢查鎖模式是一種非常好的單例實(shí)現(xiàn)模式胡嘿,解決了單例蛉艾、性能、線程安全問題,上面的雙重檢測鎖模式看上去完美無缺勿侯,其實(shí)是存在問題拓瞪,在多線程的情況下,可能會出現(xiàn)空指針問題助琐,出現(xiàn)問題的原因是JVM在實(shí)例化對象的時(shí)候會進(jìn)行優(yōu)化和指令重排序操作祭埂。什么是指令重排?兵钮,看下面這個(gè)例子蛆橡,簡單了解一下指令從排序
private SingletonObject4(){
1 int x = 10;
2 int y = 30;
3 Object o = new Object();
}
上面的構(gòu)造函數(shù)SingletonObject4(),我們編寫的順序是1掘譬、2泰演、3,JVM 會對它進(jìn)行指令重排序葱轩,所以執(zhí)行順序可能是3睦焕、1、2靴拱,也可能是2垃喊、3、1袜炕,不管是那種執(zhí)行順序缔御,JVM 最后都會保證所以實(shí)例都完成實(shí)例化。 如果構(gòu)造函數(shù)中操作比較多時(shí)妇蛀,為了提升效率耕突,JVM 會在構(gòu)造函數(shù)里面的屬性未全部完成實(shí)例化時(shí),就返回對象评架。雙重檢測鎖出現(xiàn)空指針問題的原因就是出現(xiàn)在這里眷茁,當(dāng)某個(gè)線程獲取鎖進(jìn)行實(shí)例化時(shí),其他線程就直接獲取實(shí)例使用纵诞,由于JVM指令重排序的原因上祈,其他線程獲取的對象也許不是一個(gè)完整的對象,所以在使用實(shí)例的時(shí)候就會出現(xiàn)空指針異常問題浙芙。
要解決雙重檢查鎖模式帶來空指針異常的問題登刺,只需要使用volatile關(guān)鍵字,volatile關(guān)鍵字嚴(yán)格遵循h(huán)appens-before原則嗡呼,即在讀操作前纸俭,寫操作必須全部完成。添加volatile關(guān)鍵字之后的單例模式代碼:
public class SingletonObject5 {
// 添加volatile關(guān)鍵字
private static volatile SingletonObject5 instance;
private SingletonObject5(){
}
public static SingletonObject5 getInstance(){
if (instance == null)
synchronized (SingletonObject5.class){
if (instance == null){
instance = new SingletonObject5();
}
}
return instance;
}
}
添加volatile關(guān)鍵字之后的雙重檢查鎖模式是一種比較好的單例實(shí)現(xiàn)模式南窗,能夠保證在多線程的情況下線程安全也不會有性能問題揍很。
4.靜態(tài)內(nèi)部類單例模式
靜態(tài)內(nèi)部類單例模式也稱單例持有者模式郎楼,實(shí)例由內(nèi)部類創(chuàng)建,由于 JVM 在加載外部類的過程中, 是不會加載靜態(tài)內(nèi)部類的, 只有內(nèi)部類的屬性/方法被調(diào)用時(shí)才會被加載, 并初始化其靜態(tài)屬性窒悔。靜態(tài)屬性由static修飾呜袁,保證只被實(shí)例化一次,并且嚴(yán)格保證實(shí)例化順序简珠。靜態(tài)內(nèi)部類單例模式代碼如下:
public class SingletonObject6 {
private SingletonObject6(){
}
// 單例持有者
private static class InstanceHolder{
private final static SingletonObject6 instance = new SingletonObject6();
}
public static SingletonObject6 getInstance(){
// 調(diào)用內(nèi)部類屬性
return InstanceHolder.instance;
}
}
靜態(tài)內(nèi)部類單例模式是一種優(yōu)秀的單例模式阶界,是開源項(xiàng)目中比較常用的一種單例模式。在沒有加任何鎖的情況下聋庵,保證了多線程下的安全荐操,并且沒有任何性能影響和空間的浪費(fèi)。
5.枚舉類實(shí)現(xiàn)單例模式
枚舉類實(shí)現(xiàn)單例模式是 effective java 作者極力推薦的單例實(shí)現(xiàn)模式珍策,因?yàn)槊杜e類型是線程安全的,并且只會裝載一次宅倒,設(shè)計(jì)者充分的利用了枚舉的這個(gè)特性來實(shí)現(xiàn)單例模式攘宙,枚舉的寫法非常簡單拐迁,而且枚舉類型是所用單例實(shí)現(xiàn)中唯一一種不會被破壞的單例實(shí)現(xiàn)模式。
public class SingletonObject7 {
private SingletonObject7(){
}
/**
* 枚舉類型是線程安全的,并且只會裝載一次
*/
private enum Singleton{
INSTANCE;
private final SingletonObject7 instance;
Singleton(){
instance = new SingletonObject7();
}
private SingletonObject7 getInstance(){
return instance;
}
}
public static SingletonObject7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
}
破壞單例模式的方法及解決辦法
- 除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調(diào)用構(gòu)造方法生成新的對象讯壶,所以如果我們想要阻止單例破壞立轧,可以在構(gòu)造方法中進(jìn)行判斷氛改,若已有實(shí)例, 則阻止生成新的實(shí)例,解決辦法如下:
private SingletonObject1(){
if (instance !=null){
throw new RuntimeException("實(shí)例已經(jīng)存在瑰艘,請通過 getInstance()方法獲取");
}
}
- 如果單例類實(shí)現(xiàn)了序列化接口Serializable, 就可以通過反序列化破壞單例,所以我們可以不實(shí)現(xiàn)序列化接口,如果非得實(shí)現(xiàn)序列化接口囤耳,可以重寫反序列化方法readResolve(), 反序列化時(shí)直接返回相關(guān)單例對象。
public Object readResolve() throws ObjectStreamException {
return instance;
}