單例模式的5種寫法(轉(zhuǎn)載)

原文地址:https://mp.weixin.qq.com/s/dU_Mzz76h-qQZvrgeSe44g

本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看Spring的時(shí)候又經(jīng)常會(huì)看到“單例”,“工廠”這些字樣。

所以,就先來說說單例和工廠設(shè)計(jì)模式啦,這兩種模式也是很常見的,我看很多面經(jīng)都會(huì)遇到這兩種模式~

本文主要講解單例設(shè)計(jì)模式,如果有錯(cuò)的地方希望能多多包涵,并不吝在評(píng)論區(qū)指正!

一、單例模式概述

單例模式定義很簡(jiǎn)單:一個(gè)類中能創(chuàng)建一個(gè)實(shí)例,所以稱之為單例!

那我們什么時(shí)候會(huì)用到單例模式呢??

  • 那我們想想既然一個(gè)類中只能創(chuàng)建一個(gè)實(shí)例了,那么可以說這是跟類的狀態(tài)與對(duì)象無關(guān)的了。

  • 頻繁創(chuàng)建對(duì)象、管理對(duì)象是一件耗費(fèi)資源的事,我們只需要?jiǎng)?chuàng)建一個(gè)對(duì)象來用就足夠了!

學(xué)過Java Web的同學(xué)可能就知道:

  • Servlet是單例的

  • Struts2是多例的

  • SpringMVC是單例的

那既然多例是頻繁創(chuàng)建對(duì)象、需要管理對(duì)象的,那Struts2為什么要多例呢??

  • 主要由于設(shè)計(jì)層面上的問題,Struts2是基于Filter攔截類的,ognl引擎對(duì)變量是注入的。所以它要設(shè)計(jì)成多例的~

能使用一個(gè)對(duì)象來做就不用實(shí)例化多個(gè)對(duì)象!這就能減少我們空間和內(nèi)存的開銷~

那有可能有的人又會(huì)想了:我們使用靜態(tài)類.doSomething()和使用單例對(duì)象調(diào)用方法的效果是一樣的啊。

  • 沒錯(cuò),效果就是一樣的。使用靜態(tài)類.doSomething()體現(xiàn)的是基于對(duì)象,而使用單例設(shè)計(jì)模式體現(xiàn)的是面向?qū)ο?/strong>。

二、編寫單例模式的代碼

編寫單例模式的代碼其實(shí)很簡(jiǎn)單,就分了三步:

  • 將構(gòu)造函數(shù)私有化

  • 在類的內(nèi)部創(chuàng)建實(shí)例

  • 提供獲取唯一實(shí)例的方法

2.1餓漢式

根據(jù)上面的步驟,我們就可以輕松完成創(chuàng)建單例對(duì)象了。

public class Java3y {    // 1.將構(gòu)造函數(shù)私有化,不可以通過new的方式來創(chuàng)建對(duì)象    private Java3y(){}    // 2.在類的內(nèi)部創(chuàng)建自行實(shí)例    private static Java3y java3y = new Java3y();    // 3.提供獲取唯一實(shí)例的方法    public static Student getJava3y() {        return java3y;    }}

這種代碼我們稱之為:“餓漢式”:

  • 一上來就創(chuàng)建對(duì)象了,如果該實(shí)例從始至終都沒被使用過,則會(huì)造成內(nèi)存浪費(fèi)。

2.2簡(jiǎn)單懶漢式

既然說一上來就創(chuàng)建對(duì)象,如果沒有用過會(huì)造成內(nèi)存浪費(fèi):

  • 那么我們就設(shè)計(jì)用到的時(shí)候再創(chuàng)建對(duì)象!
public class Java3y {    // 1.將構(gòu)造函數(shù)私有化,不可以通過new的方式來創(chuàng)建對(duì)象    private Java3y(){}    // 2.1先不創(chuàng)建對(duì)象,等用到的時(shí)候再創(chuàng)建    private static Java3y java3y = null;    // 2.1調(diào)用到這個(gè)方法了,證明是要被用到的了    public static Java3y getJava3y() {        // 3\. 如果這個(gè)對(duì)象引用為null,我們就創(chuàng)建并返回出去        if (java3y == null) {            java3y = new Java3y();        }        return java3y;    }}

上面的代碼行不行??在單線程環(huán)境下是行的,在多線程環(huán)境下就不行了!

要解決也很簡(jiǎn)單,我們只要加鎖就行了:

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"></figcaption>

2.3雙重檢測(cè)機(jī)制(DCL)懶漢式

上面那種直接在方法上加鎖的方式其實(shí)不夠好,因?yàn)樵?strong>方法上加了內(nèi)置鎖在多線程環(huán)境下性能會(huì)比較低下,所以我們可以將鎖的范圍縮小。

public class Java3y {    private Java3y() {    }    private static Java3y java3y = null;    public static Java3y getJava3y() {        if (java3y == null) {            // 將鎖的范圍縮小,提高性能            synchronized (Java3y.class) {                java3y = new Java3y();            }        }        return java3y;    }}

那上面的代碼可行嗎??不行,因?yàn)殡m然加了鎖,但還是有可能創(chuàng)建出兩個(gè)對(duì)象出來的:

  • 線程A和線程B同時(shí)調(diào)用getJava3y()方法,他們同時(shí)判斷java==null,得出的結(jié)果都是為null,所以進(jìn)入了if代碼塊了

  • 此時(shí)線程A得到CPU的控制權(quán)-->進(jìn)入同步代碼塊-->創(chuàng)建對(duì)象-->返回對(duì)象

  • 線程A完成了以后,此時(shí)線程B得到了CPU的控制權(quán)。同樣是-->進(jìn)入同步代碼塊-->創(chuàng)建對(duì)象-->返回對(duì)象

  • 很明顯的是:Java3y類返回了不止一個(gè)實(shí)例!所以上面的代碼是不行的!

有的同學(xué)可能覺得我瞎吹比,明明加鎖了還不行?我們來測(cè)試一下:

public class TestDemo {    public static void main(String[] args) {        // 線程A        new Thread(() -> {            // 創(chuàng)建單例對(duì)象            Java3y java3y = Java3y.getJava3y();            System.out.println(java3y);        }).start();        // 線程B        new Thread(() -> {            // 創(chuàng)建單例對(duì)象            Java3y java3y = Java3y.getJava3y();            System.out.println(java3y);        }).start();        // 線程C        new Thread(() -> {            // 創(chuàng)建單例對(duì)象            Java3y java3y = Java3y.getJava3y();            System.out.println(java3y);        }).start();    }}

可以看到,打印出的對(duì)象不單單只有一個(gè)的!

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"></figcaption>

厲害的程序員又想到了:進(jìn)入同步代碼塊時(shí)再判斷一下對(duì)象是否存在就穩(wěn)了吧!

  • 所以,有了下面的代碼
public class Java3y {    private Java3y() {    }    private static Java3y java3y = null;    public static Java3y getJava3y() {        if (java3y == null) {            // 將鎖的范圍縮小,提高性能            synchronized (Java3y.class) {                // 再判斷一次是否為null                if (java3y == null) {                    java3y = new Java3y();                }            }        }        return java3y;    }}

其實(shí)還不穩(wěn)!這里會(huì)有重排序的問題

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"></figcaption>

本來想測(cè)試重排序問題的效果的,一直沒測(cè)試出來~~~有相關(guān)測(cè)試代碼的希望可以告訴我怎么能測(cè)出來….

要解決也十分簡(jiǎn)單,加上我們的volatile關(guān)鍵字就可以了,volatile有內(nèi)存屏障的功能!

具體可參考資料:

所以說,完整的DCL代碼是這樣子的:

public class Java3y {    private Java3y() {    }    private static volatile Java3y java3y = null;    public static Java3y getJava3y() {        if (java3y == null) {            // 將鎖的范圍縮小,提高性能            synchronized (Java3y.class) {                // 再判斷一次是否為null                if (java3y == null) {                    java3y = new Java3y();                }            }        }        return java3y;    }}

再說明:

image

<figcaption style="margin: 10px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"></figcaption>

2.4靜態(tài)內(nèi)部類懶漢式

還可以使用靜態(tài)內(nèi)部類這種巧妙的方式來實(shí)現(xiàn)單例模式!它的原理是這樣的:

  • 當(dāng)任何一個(gè)線程第一次調(diào)用getInstance()時(shí),都會(huì)使SingletonHolder被加載和被初始化,此時(shí)靜態(tài)初始化器將執(zhí)行Singleton的初始化操作。(被調(diào)用時(shí)才進(jìn)行初始化!)

  • 初始化靜態(tài)數(shù)據(jù)時(shí),Java提供了的線程安全性保證。(所以不需要任何的同步)

public class Java3y {    private Java3y() {    }    // 使用內(nèi)部類的方式來實(shí)現(xiàn)懶加載    private static class LazyHolder {        // 創(chuàng)建單例對(duì)象        private static final Java3y INSTANCE = new Java3y();    }    // 獲取對(duì)象    public static final Java3y getInstance() {        return LazyHolder.INSTANCE;    }}

靜態(tài)內(nèi)部類這種方式是非常推薦使用的!很多人沒接觸過單例模式的都不知道有這種寫法,這種寫法很優(yōu)化也高效!

參考資料:

2.5枚舉方式實(shí)現(xiàn)

使用枚舉就非常簡(jiǎn)單了:

public enum Java3y3y {    JAVA_3_Y_3_Y,}

那這種有啥好處??枚舉的方式實(shí)現(xiàn):

  • 簡(jiǎn)單,直接寫就行了

  • 防止多次實(shí)例化,即使是在面對(duì)復(fù)雜的序列化或者反射攻擊的時(shí)候(安全)!

這種也較為推薦使用!

三、總結(jié)

總的來說單例模式寫法有5種:

  • 餓漢式

  • 簡(jiǎn)單懶漢式(在方法加鎖)

  • DCL雙重檢測(cè)加鎖(進(jìn)階懶漢式)

  • 靜態(tài)內(nèi)部類實(shí)現(xiàn)懶漢式(最推薦寫法)

  • 枚舉方式(最安全、簡(jiǎn)潔寫法)

明天估計(jì)寫的是工廠模式了,敬請(qǐng)期待哦~~~

參考資料:

如果文章有錯(cuò)的地方歡迎指正,大家互相交流。習(xí)慣在微信看技術(shù)文章,想要獲取更多的Java資源的同學(xué),可以關(guān)注微信公眾號(hào):Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支持了!希望能多介紹給其他有需要的朋友

文章的目錄導(dǎo)航

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容