單例模式的定義是保證一個類僅有一個實例饲握,并提供一個全局訪問點煌珊。一般用在工具類号俐、應用配置、數(shù)據(jù)庫連接池的創(chuàng)建上定庵。
優(yōu)點是一個類在內(nèi)存里只有一個實例吏饿,減少內(nèi)存開銷踪危,可以避免對資源的多重占用。
缺點是沒有借口猪落,無法擴展贞远。
單例模式的重點:私有構(gòu)造器、線程安全笨忌、延遲加載蓝仲、序列化和序列化安全、防止反射攻擊官疲。
單例模式分為餓漢模式和懶漢模式袱结。簡單的餓漢模式和懶漢模式的創(chuàng)建如圖。
餓漢模式是在類被加載時實例就已經(jīng)被創(chuàng)建途凫。若系統(tǒng)從始至終都未調(diào)用這個實例垢夹,則會造成資源浪費,再或者這個類的實例初始化比較占用資源维费,多個類都在加載時創(chuàng)建實例就會造成系統(tǒng)服務啟動慢果元。因此可以使用延遲加載,即初次調(diào)用時再初始化實例犀盟。注意的是兩種模式都需要將構(gòu)造器私有化而晒。
線程安全
而上圖所示懶漢模式是線程不安全的,如果只有一個線程調(diào)用這個類阅畴,的確只會初始化一個類欣硼。但如果多線程同時獲取實例,當兩個線程同時到達第10行恶阴,即判斷l(xiāng)azySingleton==null的時候诈胜,此時實例都未初始化,兩個線程同時判斷為true冯事,同時進入11行焦匈,去初始化實例,就會在過程中這個對象new了兩次£墙觯現(xiàn)在寫一個測試方法缓熟。
當直接執(zhí)行時,看到兩個線程獲取到的都是一個實例摔笤。
現(xiàn)在够滑,在懶漢模式的第10行判斷實例是否為空除打上斷點,并設置斷點為線程生效吕世。
debug執(zhí)行測試main函數(shù)彰触,兩個線程都執(zhí)行到第10行,都單步調(diào)試到第11行命辖,此時兩個線程都執(zhí)行結(jié)束况毅》直停可以看到debug干預下能復現(xiàn)可能出現(xiàn)的問題:過程中存在兩個實例。
如何消除這個隱患尔许?
synchronized給方法加鎖
第一種方式是在獲取實例的方法加上關鍵字synchronized么鹤,給方法加鎖,讓方法變成同步方法味廊。而此處獲取獲取實例的方法是靜態(tài)方法蒸甜,則鎖住的是這個類。多線程時余佛,一個線程進入這個方法時迅皇,其他線程就無法進入,處于等待狀態(tài)衙熔,鎖釋放后才能進入。這樣能確保多個線程同時只有一個線程能初始化對象搅荞。
在第10行打上斷點红氯,再次調(diào)試,當一個線程進入后咕痛,選擇另一個線程痢甘,會有以下提示。
通過這種同步的方法解決了懶漢模式的線程安全問題茉贡,但是同步鎖本身比較消耗資源塞栅,有加鎖和解鎖的開銷。而且synchronized加載static方法上腔丧,鎖住的是類放椰,范圍比較大,對性能有影響愉粤。
doublecheck
第二種方式是將鎖加在方法內(nèi)部砾医,進行雙重判斷。即便兩個線程同時判斷實例為空衣厘,但接下來只有一個進程會進入鎖如蚜,并在此判斷此時實例是否為空,空則初始化對象影暴。這樣是鎖的范圍縮小错邦,降低synchronized的性能開銷。
這種寫法看上去很完美型宙,但是仍然存在隱患撬呢,問題是處在第10行和第13行。當一個線程進到第13行時妆兑,new了一個對象倾芝。雖然看上去是一步讨勤,實際上new一個對象經(jīng)歷了3個步驟。
1.分配內(nèi)存給這個對象
2.初始化對象
3.設置lazyDoubleCheckSingleton(instance)指向剛分配的內(nèi)存地址
而第二步和第三步可能會被重排序晨另。即先分配內(nèi)存給對象潭千,再將instanc指向剛分配的地址,此時對象還未初始化完成借尿,另一個進程在第10行進行空判斷的時候判斷l(xiāng)azyDoubleCheckSingleton不為空刨晴,結(jié)果return回去的仍然是空。
java語言規(guī)范中說路翻,所有程序在執(zhí)行java程序時狈癞,必須遵守intra-thread semantics這個規(guī)定,允許哪些保證重排序?qū)τ趩尉€程不會改變程序執(zhí)行結(jié)果的重排序茂契,從而提高執(zhí)行性能蝶桶。
解決重排序帶來的隱患
一種解決辦法是禁止這種重排序,做法就是聲明instance的時候加上volatile關鍵字掉冶。通過volatile和doublecheck的這個方法既兼顧了性能又兼顧了線程安全真竖。對于這個字段的理解可以參考:https://www.cnblogs.com/zhengbin/p/5654805.html
而另一種方法,不禁止重排序厌小,而是基于靜態(tài)內(nèi)部類初始化的解決方案恢共。即StaticInnerClassSingleton的instance是在靜態(tài)內(nèi)部類中被初始化的。而靜態(tài)內(nèi)部類InnerClass本身被加載時jvm會給這個內(nèi)部類上鎖璧亚,而staticInnerClassSingleton在初始化賦值的過程中即使發(fā)生重排序讨韭,其他線程也無法獲取實例。只有InnerClass被加載完成后癣蟋,其他線程才能訪問透硝。這種方式實現(xiàn)了延遲加載,降低創(chuàng)建實例帶來的開銷疯搅,也能兼顧線程安全蹬铺。