女朋友驚掉下巴問我:?jiǎn)卫J骄褂衅叻N寫法?
前言
接下來,我們要進(jìn)入的是設(shè)計(jì)模式篇,關(guān)于設(shè)計(jì)模式,作為程序員的你,肯定在工作中或者面試中遇到過很多次了吧
記得當(dāng)時(shí)18年上大三的時(shí)候出去找實(shí)習(xí),也問過了解哪些設(shè)計(jì)模式,不過我個(gè)人回答的最多的最詳細(xì)的大概也就是單例模式了,因?yàn)槲矣X得這個(gè)應(yīng)該是最最好理解的了,雖然有很多種寫法,這是為了解決不同環(huán)境下的不同問題,當(dāng)時(shí)我應(yīng)該是把懶漢、餓漢直接都手撕了一遍,也簡(jiǎn)單的把懶漢和餓漢的區(qū)別說了說
當(dāng)時(shí)令我吃驚的是面試官告訴我,單例模式其實(shí)有七種寫法,甚至可以更多,我當(dāng)時(shí)驚得下巴都掉了,當(dāng)時(shí)我就感覺到了這個(gè)行業(yè)滿滿的挑戰(zhàn)和滿滿的知識(shí)等著我學(xué)習(xí)
果不其然,現(xiàn)在越學(xué)越覺得自己廢物,越學(xué)越感覺自己有太多不會(huì)的了,不過這個(gè)路肯定還是要走下去的,撥開云霧見天明,堅(jiān)持下去吧
接下來我們來簡(jiǎn)單介紹下單例模式
單例模式,顧名思義,就是唯一的實(shí)例。在當(dāng)前進(jìn)程中,有且只有一個(gè)單例模式創(chuàng)建的類對(duì)象
比如生活中的太陽、只能有一個(gè)吧,所以只能有一個(gè)實(shí)例,這個(gè)例子要是用在當(dāng)年后羿射箭之前不合適,但是現(xiàn)在應(yīng)該還算是合適的吧
再比如寫一個(gè)校園管理系統(tǒng),有一個(gè)校長(zhǎng)的角色,只能有一個(gè),這個(gè)對(duì)象在該系統(tǒng)中做成單例就比較合適(其余的是副校長(zhǎng)的 親
這個(gè)模式應(yīng)該是大家最常見的,也是大家認(rèn)為最簡(jiǎn)單的了吧,但是實(shí)際上這個(gè)模式里面還是有很多細(xì)節(jié)的,也有很多的點(diǎn)值得大家思考的,待會(huì)咱們一起看各種寫法的時(shí)候大家記得帶著你的思考和你的問題去學(xué)習(xí)
正文
單例模式特點(diǎn)
單例模式有如下的特點(diǎn):
1、一個(gè)JVM中有且只有一個(gè)實(shí)例的存在,構(gòu)造器私有,外部無法創(chuàng)建該實(shí)例
2、提供一個(gè)公開的get方法獲得唯一的這個(gè)實(shí)例
有哪些優(yōu)點(diǎn)呢:
1、省去了new的操作,降低系統(tǒng)內(nèi)存的使用頻率,減輕GC的壓力
2、系統(tǒng)中的一些類需要全局單例,比如spring中的controller,再比如人類的太陽
3、避免了資源的重復(fù)的占用,減少了內(nèi)存的開銷
其實(shí)也是有一些缺點(diǎn)的:
沒有接口,不可繼承與單一職責(zé)原則沖突,一個(gè)類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實(shí)例化
先把要介紹的七種給大家說一下,大家有個(gè)印象
餓漢式、懶漢式線程不安全和安全版、DCL雙重檢測(cè)鎖模式的線程不安全和安全版、靜態(tài)內(nèi)部類、枚舉類
大家先聽個(gè)耳熟,下面一一介紹
餓漢式
餓漢式,就是比較餓,于是乎吃的比較早,也就是創(chuàng)建的比較早,會(huì)隨著JVM的啟動(dòng)而初始化該單例
也正是由于這種類裝載的時(shí)候就完成了單例的實(shí)例化了,不存在所謂的線程安全問題,是線程安全的,相應(yīng)的缺點(diǎn)就是未達(dá)到lazy loading的效果,如果創(chuàng)建的這個(gè)單例類始終未用到,便回造成資源浪費(fèi)
其實(shí)在實(shí)際開發(fā)中,即使知道一定用得到,我們一般也不太會(huì)使用這種機(jī)制,因?yàn)槿绻麊卫龑?duì)象很多,會(huì)影響啟動(dòng)的速度,采用懶加載機(jī)制是比較節(jié)約資源的
開發(fā)中很多思想也是采用懶加載,只有當(dāng)真正用到一個(gè)東西的時(shí)候才允許它占用相應(yīng)的資源
- /**
- * 餓漢式:通過classloader機(jī)制避免了多線程的同步問題,在類裝載的時(shí)候完成實(shí)例化
- * 優(yōu)點(diǎn):寫法簡(jiǎn)單,類裝載的時(shí)候完成實(shí)例化,避免了線程同步的問題
- * 缺點(diǎn):未達(dá)到lazy loading的效果,如果始終未用到則可能造成資源浪費(fèi)
- * 適用場(chǎng)景:
- */
- public class HungrySingleton {
- //1、構(gòu)造器私有化
- private HungrySingleton(){}
- //2、類的內(nèi)部創(chuàng)建對(duì)象的實(shí)例
- private final static HungrySingleton dayu = new HungrySingleton();
- //3、將類的內(nèi)部實(shí)例提供一個(gè)靜態(tài)方法返回出去
- private static HungrySingleton getInstance(){
- return dayu;
- }
- }
懶漢式(線程不安全、線程安全)
懶漢式咯,就是比較懶,在啟動(dòng)的時(shí)候,不會(huì)進(jìn)行該單例對(duì)象的創(chuàng)建,只有當(dāng)真正用到的時(shí)候才會(huì)去加載這些東西
之所以加懶漢式,大概就是采用了懶加載思想
我們看下面這個(gè)懶漢式的代碼
- /**
- * 懶漢式
- * 缺點(diǎn):線程不安全,工作中一般不用
- */
- public class NotSafeLazySingleton {
- //構(gòu)造器私有化
- private NotSafeLazySingleton(){}
- //暫時(shí)不加載實(shí)例
- private static NotSafeLazySingleton dayu;
- /**
- * 存在線程安全問題
- * 線程A到括號(hào)dayu == null判斷完之后,進(jìn)入括號(hào)內(nèi)部,
- * 此時(shí)線程B獲得執(zhí)行權(quán),判斷==null也是true,所以也進(jìn)入
- * 此時(shí)兩個(gè)線程便出現(xiàn)了兩個(gè)dayu對(duì)象
- * @return
- */
- public static NotSafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new NotSafeLazySingleton();
- }
- return dayu;
- }
- }
其實(shí)有過多線程的經(jīng)驗(yàn)的小伙伴應(yīng)該很快就看出來了,上面這種懶漢式是有線程安全問題的,當(dāng)線程A執(zhí)行到if(dayu == null)這一行的時(shí)候,判斷為空,true進(jìn)入括號(hào)內(nèi)部,此時(shí)線程A的時(shí)間片用完了,到了線程B的執(zhí)行了,于是乎也會(huì)判斷為空,進(jìn)入括號(hào)內(nèi)部
線程B創(chuàng)建了一個(gè)NotSafeLazySingleton對(duì)象,輪到線程A執(zhí)行的時(shí)候,由于在之前已經(jīng)判斷完進(jìn)入了括號(hào)內(nèi)部,于是線程A也會(huì)創(chuàng)建一個(gè)NotSafeLazySingleton對(duì)象
GG,這樣不是我們想要的效果,這就不屬于單例模式了,所以這種在多線程情況下是存在安全問題的
有了問題,自然就是解決咯,可能有的小伙伴也想到了,存在線程安全問題,那就加上線程安全關(guān)鍵字synchronized來解決,于是乎便有了下面的代碼,我們給函數(shù)加上關(guān)鍵字synchronized,但是這樣會(huì)造成效率極其低下
所有調(diào)用這個(gè)方法去使用單例對(duì)象的地方都需要排隊(duì)阻塞知道該鎖的釋放,在多線程情況下會(huì)迅速降低效率
- /**
- * 懶漢式安全寫法
- * 缺點(diǎn):Synchronized關(guān)鍵字導(dǎo)致方法效率低 效率極低
- * 優(yōu)點(diǎn):線程安全
- * 適用場(chǎng)景:實(shí)際開發(fā) 不推薦使用
- */
- public class SafeLazySingleton {
- //構(gòu)造器私有化
- private SafeLazySingleton(){}
- //暫時(shí)不加載實(shí)例
- private static SafeLazySingleton dayu;
- /**
- * synchronized導(dǎo)致所有通過該方法獲取該對(duì)象的時(shí)候都要排隊(duì)
- */
- public static synchronized SafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new SafeLazySingleton();
- }
- return dayu;
- }
- }
所有調(diào)用這個(gè)方法去使用單例對(duì)象的地方都需要排隊(duì)阻塞知道該鎖的釋放,在多線程情況下會(huì)迅速降低效率,于是有了下面的這種改進(jìn)方法
只鎖其中的部分代碼,看下下面的代碼
- /**
- * 本意上是對(duì)SafeLazySingelton的改進(jìn) 因?yàn)榍懊娴膶?duì)整個(gè)方法進(jìn)行加鎖的效率實(shí)在是太低了
- * 但是這種還是不能起到線程同步的作用 和NotSafeLazySingelton類似 只要線程進(jìn)入了== null的里面
- * 此時(shí)另一個(gè)線程獲得CPU分配的時(shí)間片 則會(huì)出現(xiàn)多個(gè)對(duì)象
- */
- public class NotSafeLaySingleton2 {
- //構(gòu)造器私有化
- private NotSafeLaySingleton2(){}
- //暫時(shí)不加載實(shí)例
- private static NotSafeLaySingleton2 dayu;
- /**
- * @return
- */
- public static NotSafeLaySingleton2 getInstance(){
- if(dayu == null){
- synchronized (NotSafeLaySingleton2.class){
- dayu = new NotSafeLaySingleton2();
- }
- }
- return dayu;
- }
- }
上面的這種代碼看著有問題嗎?
不知道你認(rèn)真讀了上面代碼之后,內(nèi)心是怎么想的,聰明的小伙伴已經(jīng)發(fā)現(xiàn)了事情不是這么簡(jiǎn)單,發(fā)現(xiàn)其中了問題
是的,上面的這種改進(jìn)方法,貌似實(shí)現(xiàn)了效率跟高些,但是會(huì)隨之帶來多線程的問題
線程A判斷dayu == null進(jìn)入括號(hào),還沒拿到NotSafeLaySingleton2的鎖,時(shí)間片消耗完了,此時(shí)線程B也判斷,發(fā)現(xiàn)dayu == null也成立,此時(shí)也會(huì)進(jìn)入括號(hào),假設(shè)線程B拿到了鎖,創(chuàng)建了一個(gè)NotSafeLaySingleton2對(duì)象,執(zhí)行完之后釋放鎖。線程A拿到該鎖,會(huì)重新創(chuàng)建一個(gè)對(duì)象,于是出現(xiàn)多例現(xiàn)象
先是通過synchronized加在方法層面解決并發(fā)問題,但是隨之而來帶來效率問題,于是為了提高效率,加在內(nèi)部,但是加在內(nèi)部就有了相應(yīng)的線程安全問題
說了這么多,就是要引出我們下面的線程安全的DCL的單例模式
看下怎么寫
雙重檢查鎖模式DCL- double chechked locking(線程安全)
上面那個(gè)其實(shí)屬于單重檢查鎖模式,我起的名字,因?yàn)橹粰z查了一個(gè)地方的鎖,正是如此也帶來了多線程的問題,于是乎就有了下面這種雙重檢測(cè)形勢(shì)的單例模式了,一起看看吧,穩(wěn)得一批
- /**
- * 雙重檢測(cè)單例:穩(wěn)得一批
- * 優(yōu)點(diǎn):線程安全 延遲加載 效率相對(duì)來說也不錯(cuò)
- *使用場(chǎng)景:實(shí)際開發(fā)中 用的比較多
- */
- public class DoubleCheckSingleton {
- private static volatile DoubleCheckSingleton dayu;
- private DoubleCheckSingleton(){}
- /**
- * 解決線程安全的問題同時(shí) 也解決懶加載問題
- * @return
- */
- public static DoubleCheckSingleton getInstance(){
- if(dayu == null){
- synchronized (DoubleCheckSingleton.class){
- if(dayu == null){
- dayu = new DoubleCheckSingleton();
- }
- }
- }
- return dayu;
- }
- }
上面這種在進(jìn)入了data == null的內(nèi)部也會(huì)再次判斷一次是否還等于空,這種就很好的解決了多線程的問題
這種DCL的單例模式在工作中算是常用的一種了,有效的解決高并發(fā)下的單例模式問題
靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類加載單例,類加載機(jī)制保證線程安全,而且還有一個(gè)優(yōu)點(diǎn),懶加載,只有在調(diào)用getInstance的時(shí)候才會(huì)加載內(nèi)部類,才會(huì)創(chuàng)建這個(gè)對(duì)象
外部類被裝載的時(shí)候,內(nèi)部類不會(huì)立即被裝載,調(diào)用getInstance才會(huì)裝載,并且只會(huì)裝載一次,且不存在線程安全問題
- /**
- * 靜態(tài)內(nèi)部類加載單例
- * 優(yōu)點(diǎn):類裝載機(jī)制保證線程安全 懶加載 只有調(diào)用getInstance才會(huì)加載內(nèi)部類
- * 適用場(chǎng)景:
- */
- public class StaticInnerClassSingleton {
- private StaticInnerClassSingleton(){}
- /**
- * 1、外部類被裝載時(shí) 內(nèi)部不會(huì)立即被裝載
- * 2、調(diào)用getInstance方法時(shí)會(huì)裝載 只會(huì)裝載一次 且不存在線程安全
- */
- private static class SingletonInstance{
- private static final StaticInnerClassSingleton dayu = new StaticInnerClassSingleton();
- }
- //返回靜態(tài)內(nèi)部類中的對(duì)象
- public static StaticInnerClassSingleton getInstance(){
- return SingletonInstance.dayu;
- }
- }
枚舉類
枚舉類也是可以用作單例模式,而且還很簡(jiǎn)單
Effective Java作者Josh Bloch所提倡的單例實(shí)現(xiàn)的方式就是這種,這種無線程安全問題,還可以防止反序列化重新創(chuàng)建新的對(duì)象
- /**
- * 枚舉實(shí)現(xiàn)單例
- * 優(yōu)點(diǎn):簡(jiǎn)潔 無線程安全問題 還可以防止反序列化重新創(chuàng)建新的對(duì)象
- * Effective Java作者Josh Bloch提倡的方法
- */
- public class EnumSingleton {
- public static void main(String[] args) {
- //instance和instance2是同一個(gè)對(duì)象
- Singleton instance = Singleton.INSTANCE;
- Singleton instance2 = Singleton.INSTANCE;
- }
- enum Singleton{
- INSTANCE;
- }
- }
總結(jié)
設(shè)計(jì)模式應(yīng)該屬于面試高頻,而單例模式又是設(shè)計(jì)模式的最簡(jiǎn)單,或者說是最常見的設(shè)計(jì)模式之一,看完這篇文章,大家應(yīng)該都知道單例模式的多種寫法了,也知道各種的優(yōu)劣勢(shì)和相應(yīng)的使用場(chǎng)景了
我們思考一個(gè)問題,為什么要使用單例模式而使用靜態(tài)方法
這兩個(gè)其實(shí)都可以實(shí)現(xiàn)我們加載的最終目的,但是他們一個(gè)是基于對(duì)象的,一個(gè)是屬于面向?qū)ο蟮?,就像是很多種情況,我們通過普通的編碼也可以實(shí)現(xiàn),但是我們引入設(shè)計(jì)模式來更好的體現(xiàn)編程思想
如果一個(gè)方法和他所在的類的實(shí)例對(duì)象確實(shí)是無關(guān)的,那么它就應(yīng)該是靜態(tài)的,反之它就應(yīng)該是非靜態(tài)的,如果我們需要使用非靜態(tài)的方法,但是在創(chuàng)建類對(duì)象的時(shí)候,又只需要維護(hù)一個(gè)實(shí)例,不想創(chuàng)建多個(gè)不同的實(shí)例,就需要使用單例模式了。