java中的單例模式和Singleton是什么?這篇文章運(yùn)用了實(shí)例代碼展示,代碼非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
站在用戶的角度思考問題,與客戶深入溝通,找到東鄉(xiāng)網(wǎng)站設(shè)計(jì)與東鄉(xiāng)網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗(yàn)好的作品,建站類型包括:網(wǎng)站設(shè)計(jì)制作、成都做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、申請域名、網(wǎng)站空間、企業(yè)郵箱。業(yè)務(wù)覆蓋東鄉(xiāng)地區(qū)。一、什么是單例模式
【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實(shí)例,他是屬于創(chuàng)建類型的一種常用的軟件設(shè)計(jì)模式。通過單例模式的方法創(chuàng)建的類在當(dāng)前進(jìn)程中只有一個實(shí)例(根據(jù)需要,也有可能一個線程中屬于單例,如:僅線程上下文內(nèi)使用同一個實(shí)例)。
1、單例類只能有一個實(shí)例。
2、單例類必須自己創(chuàng)建自己的唯一實(shí)例。
3、單例類必須給所有其他對象提供這一實(shí)例。
那咱們大概知道了,其實(shí)說白了,就是我們整個項(xiàng)目周期內(nèi),只會有一個實(shí)例,當(dāng)項(xiàng)目停止的時候,實(shí)例銷毀,當(dāng)重新啟動的時候,我們的實(shí)例又會產(chǎn)品。
上文中說到了一個名詞【創(chuàng)建類型】的設(shè)計(jì)模式,那什么是創(chuàng)建類型的設(shè)計(jì)模式呢?
創(chuàng)建型(Creational)模式:負(fù)責(zé)對象創(chuàng)建,我們使用這個模式,就是為了創(chuàng)建我們需要的對象實(shí)例的。
那除了創(chuàng)建型還有其他兩種類型的模式:
結(jié)構(gòu)型(Structural)模式:處理類與對象間的組合
行為型(Behavioral)模式:類與對象交互中的職責(zé)分
這兩種設(shè)計(jì)模式,以后會慢慢說到,這里先按下不表。
咱們就重點(diǎn)從0開始分析分析如何創(chuàng)建一個單例模式的對象實(shí)例。
二、如何創(chuàng)建單例模式
實(shí)現(xiàn)單例模式有很多方法:從“懶漢式”到“餓漢式”,最后“雙檢鎖”模式,這里咱們就慢慢的,從一步一步的開始講解如何創(chuàng)建單例。
1、正常的思考邏輯順序
既然要創(chuàng)建單一的實(shí)例,那我們首先需要學(xué)會如何去創(chuàng)建一個實(shí)例,這個很簡單,相信每個人都會創(chuàng)建實(shí)例,就比如說這樣的:
////// 定義一個天氣類 /// public class WeatherForecast { public WeatherForecast() { Date = DateTime.Now; } public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } } [HttpGet] public WeatherForecast Get() { // 實(shí)例化一個對象實(shí)例 WeatherForecast weather = new WeatherForecast(); return weather; }
我們每次訪問的時候,時間都是會變化,所以我們的實(shí)例也是一直在創(chuàng)建,在變化:
相信每個人都能看到這個代碼是什么意思,不多說,直接往下走,我們知道,單例模式的核心目的就是:
必須保證這個實(shí)例在整個系統(tǒng)的運(yùn)行周期內(nèi)是唯一的,這樣可以保證中間不會出現(xiàn)問題。
那好,我們改進(jìn)改進(jìn),不是說要唯一一個么,好說!我直接返回不就行了:
////// 定義一個天氣類 /// public class WeatherForecast { // 定義一個靜態(tài)變量來保存類的唯一實(shí)例 private static WeatherForecast uniqueInstance; // 定義私有構(gòu)造函數(shù),使外界不能創(chuàng)建該類實(shí)例 private WeatherForecast() { Date = DateTime.Now; } ////// 靜態(tài)方法,來返回唯一實(shí)例 /// 如果存在,則返回 /// ///public static WeatherForecast GetInstance() { // 如果類的實(shí)例不存在則創(chuàng)建,否則直接返回 // 其實(shí)嚴(yán)格意義上來說,這個不屬于【單例】 if (uniqueInstance == null) { uniqueInstance = new WeatherForecast(); } return uniqueInstance; } public DateTime Date { get; set; }public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } }
然后我們修改一下調(diào)用方法,因?yàn)槲覀兊哪J(rèn)構(gòu)造函數(shù)已經(jīng)私有化了,不允許再創(chuàng)建實(shí)例了,所以我們直接這么調(diào)用:
[HttpGet] public WeatherForecast Get() { // 實(shí)例化一個對象實(shí)例 WeatherForecast weather = WeatherForecast.GetInstance(); return weather; }
最后來看看效果:
這個時候,我們可以看到,時間已經(jīng)不發(fā)生變化了,也就是說我們的實(shí)例是唯一的了,大功告成!是不是很開心!
但是,別著急,問題來了,我們目前是單線程的,所以只有一個,那如果多線程呢,如果多個線程同時訪問,會不會也會正常呢?
這里我們做一個測試,我們在項(xiàng)目啟動的時候,用多線程去調(diào)用:
[HttpGet] public WeatherForecast Get() { // 實(shí)例化一個對象實(shí)例 //WeatherForecast weather = WeatherForecast.GetInstance(); // 多線程去調(diào)用 for (int i = 0; i < 3; i++) { var th = new Thread( new ParameterizedThreadStart((state) => { WeatherForecast.GetInstance(); }) ); th.Start(i); } return null; }
然后我們看看效果是怎樣的,按照我們的思路,應(yīng)該是只會走一遍構(gòu)造函數(shù),其實(shí)不是:
3個線程在第一次訪問GetInstance方法時,同時判斷(uniqueInstance ==null)這個條件時都返回真,然后都去創(chuàng)建了實(shí)例,這個肯定是不對的。那怎么辦呢,只要讓GetInstance方法只運(yùn)行一個線程運(yùn)行就好了,我們可以加一個鎖來控制他,代碼如下:
public class WeatherForecast { // 定義一個靜態(tài)變量來保存類的唯一實(shí)例 private static WeatherForecast uniqueInstance; // 定義一個鎖,防止多線程 private static readonly object locker = new object(); // 定義私有構(gòu)造函數(shù),使外界不能創(chuàng)建該類實(shí)例 private WeatherForecast() { Date = DateTime.Now; } ////// 靜態(tài)方法,來返回唯一實(shí)例 /// 如果存在,則返回 /// ///public static WeatherForecast GetInstance() { // 當(dāng)?shù)谝粋€線程執(zhí)行的時候,會對locker對象 "加鎖", // 當(dāng)其他線程執(zhí)行的時候,會等待 locker 執(zhí)行完解鎖 lock (locker) { // 如果類的實(shí)例不存在則創(chuàng)建,否則直接返回 if (uniqueInstance == null) { uniqueInstance = new WeatherForecast(); } } return uniqueInstance; } public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } }
這個時候,我們再并發(fā)測試,發(fā)現(xiàn)已經(jīng)都一樣了,這樣就達(dá)到了我們想要的效果,但是這樣真的是最完美的么,其實(shí)不是的,因?yàn)槲覀兗渔i,只是第一次判斷是否為空,如果創(chuàng)建好了以后,以后就不用去管這個 lock 鎖了,我們只關(guān)心的是 uniqueInstance 是否為空,那我們再完善一下:
////// 定義一個天氣類 /// public class WeatherForecast { // 定義一個靜態(tài)變量來保存類的唯一實(shí)例 private static WeatherForecast uniqueInstance; // 定義一個鎖,防止多線程 private static readonly object locker = new object(); // 定義私有構(gòu)造函數(shù),使外界不能創(chuàng)建該類實(shí)例 private WeatherForecast() { Date = DateTime.Now; } ////// 靜態(tài)方法,來返回唯一實(shí)例 /// 如果存在,則返回 /// ///public static WeatherForecast GetInstance() { // 當(dāng)?shù)谝粋€線程執(zhí)行的時候,會對locker對象 "加鎖", // 當(dāng)其他線程執(zhí)行的時候,會等待 locker 執(zhí)行完解鎖 if (uniqueInstance == null) { lock (locker) { // 如果類的實(shí)例不存在則創(chuàng)建,否則直接返回 if (uniqueInstance == null) { uniqueInstance = new WeatherForecast(); } } } return uniqueInstance; } public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } }
這樣才最終的完美實(shí)現(xiàn)我們的單例模式!搞定。
2、幽靈事件:指令重排
當(dāng)然,如果你看完了上邊的那四步已經(jīng)可以出師了,平時我們就是這么使用的,也是這么想的,但是真的就是萬無一失么,有一個 JAVA 的朋友提出了這個問題,C# 中我沒有聽說過,是我孤陋寡聞了么:
單例模式的幽靈事件,時令重排會偶爾導(dǎo)致單例模式失效。
是不是聽起來感覺很高大上,而不知所云,沒關(guān)系,咱們平時用不到,但是可以了解了解:
為何要指令重排?
指令重排是指的 volatile,現(xiàn)在的CPU一般采用流水線來執(zhí)行指令。一個指令的執(zhí)行被分成:取指、譯碼、訪存、執(zhí)行、寫回、等若干個階段。然后,多條指令可以同時存在于流水線中,同時被執(zhí)行。
指令流水線并不是串行的,并不會因?yàn)橐粋€耗時很長的指令在“執(zhí)行”階段呆很長時間,而導(dǎo)致后續(xù)的指令都卡在“執(zhí)行”之前的階段上。
相反,流水線是并行的,多個指令可以同時處于同一個階段,只要CPU內(nèi)部相應(yīng)的處理部件未被占滿即可。比如說CPU有一個加法器和一個除法器,那么一條加法指令和一條除法指令就可能同時處于“執(zhí)行”階段, 而兩條加法指令在“執(zhí)行”階段就只能串行工作。
相比于串行+阻塞的方式,流水線像這樣并行的工作,效率是非常高的。
然而,這樣一來,亂序可能就產(chǎn)生了。比如一條加法指令原本出現(xiàn)在一條除法指令的后面,但是由于除法的執(zhí)行時間很長,在它執(zhí)行完之前,加法可能先執(zhí)行完了。再比如兩條訪存指令,可能由于第二條指令命中了cache而導(dǎo)致它先于第一條指令完成。
一般情況下,指令亂序并不是CPU在執(zhí)行指令之前刻意去調(diào)整順序。CPU總是順序的去內(nèi)存里面取指令,然后將其順序的放入指令流水線。但是指令執(zhí)行時的各種條件,指令與指令之間的相互影響,可能導(dǎo)致順序放入流水線的指令,最終亂序執(zhí)行完成。這就是所謂的“順序流入,亂序流出”。
這個是從網(wǎng)上摘錄的,大概意思看看就行,理解雙檢鎖失效原因有兩個重點(diǎn)
1、編譯器的寫操作重排問題.
例 : B b = new B();
上面這一句并不是原子性的操作,一部分是new一個B對象,一部分是將new出來的對象賦值給b.
直覺來說我們可能認(rèn)為是先構(gòu)造對象再賦值.但是很遺憾,這個順序并不是固定的.再編譯器的重排作用下,可能會出現(xiàn)先賦值再構(gòu)造對象的情況.
2、結(jié)合上下文,結(jié)合使用情景.
理解了1中的寫操作重排以后,我卡住了一下.因?yàn)槲艺娌恢肋@種重排到底會帶來什么影響.實(shí)際上是因?yàn)槲铱创a看的不夠仔細(xì),沒有意識到使用場景.雙檢鎖的一種常見使用場景就是在單例模式下初始化一個單例并返回,然后調(diào)用初始化方法的方法體內(nèi)使用初始化完成的單例對象.
三、Singleton = 單例 ?
上邊我們說了很多,也介紹了很多單例的原理和步驟,那這里問題來了,我們在學(xué)習(xí)依賴注入的時候,用到的 Singleton 的單例注入,是不是和上邊說的一回事兒呢,這里咱們直接多多線程測試一下就行:
////// 定義一個心情類 /// public class Feeling { public Feeling() { Date = DateTime.Now; } public DateTime Date { get; set; } } // 單例注冊到容器內(nèi) services.AddSingleton();
這里重點(diǎn)表揚(yáng)下評論區(qū)的@我是你帥哥 小伙伴,及時的發(fā)現(xiàn)了我文章的漏洞,筆芯!
緊接著我們就控制器注入服務(wù),然后多線程測試:
private readonly ILogger_logger; private readonly Feeling _feeling; public WeatherForecastController(ILogger logger, Feeling feeling) { _logger = logger; _feeling = feeling; } [HttpGet] public WeatherForecast Get() { // 實(shí)例化一個對象實(shí)例 //WeatherForecast weather = WeatherForecast.GetInstance(); // 多線程去調(diào)用 for (int i = 0; i < 3; i++) { var th = new Thread( new ParameterizedThreadStart((state) => { //WeatherForecast.GetInstance(); // 此刻的心情 Console.WriteLine(_feeling.Date); }) ); th.Start(i); } return null; }
測試的結(jié)果,情理之中,只在我們項(xiàng)目初始化服務(wù)的時候,進(jìn)入了一次構(gòu)造函數(shù):
和我們上邊說的是一樣的, Singleton是一種單例,而且還是雙檢鎖那種, 因?yàn)榻Y(jié)論可以看出,我們使用單例模式,直接可以使用依賴注入 Sigleton 就能滿足的,很方便。
四、單例模式的優(yōu)缺點(diǎn)
【優(yōu)】、單例模式的優(yōu)點(diǎn):
(1)、保證唯一性:防止其他對象實(shí)例化,保證實(shí)例的唯一性;
(2)、全局性:定義好數(shù)據(jù)后,可以再整個項(xiàng)目種的任何地方使用當(dāng)前實(shí)例,以及數(shù)據(jù);
【劣】、單例模式的缺點(diǎn):
(1)、內(nèi)存常駐:因?yàn)閱卫纳芷谧铋L,存在整個開發(fā)系統(tǒng)內(nèi),如果一直添加數(shù)據(jù),或者是常駐的話,會造成一定的內(nèi)存消耗。
以下內(nèi)容來自百度百科:
優(yōu)點(diǎn)一、實(shí)例控制
單例模式會阻止其他對象實(shí)例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實(shí)例。
二、靈活性
因?yàn)轭惪刂屏藢?shí)例化過程,所以類可以靈活更改實(shí)例化過程。
缺點(diǎn)一、開銷
雖然數(shù)量很少,但如果每次對象請求引用時都要檢查是否存在類的實(shí)例,將仍然需要一些開銷??梢酝ㄟ^使用靜態(tài)初始化解決此問題。
二、可能的開發(fā)混淆
使用單例對象(尤其在類庫中定義的對象)時,開發(fā)人員必須記住自己不能使用new關(guān)鍵字實(shí)例化對象。因?yàn)榭赡軣o法訪問庫源代碼,因此應(yīng)用程序開發(fā)人員可能會意外發(fā)現(xiàn)自己無法直接實(shí)例化此類。
三、對象生存期
不能解決刪除單個對象的問題。在提供內(nèi)存管理的語言中(例如基于.NET Framework的語言),只有單例類能夠?qū)е聦?shí)例被取消分配,因?yàn)樗瑢υ搶?shí)例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實(shí)例,但這樣會導(dǎo)致單例類中出現(xiàn)懸浮引用。
到此為止, 關(guān)于java中的單例模式和Singleton有了一個基礎(chǔ)的認(rèn)識, 但是對于具體的使用方法還是需要多加鞏固和練習(xí),如果想了解更多相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊。