Java中單例模式的實現(xiàn)

單例模式作為23種設(shè)計模式中最常用的一種,也是面試中的???,但是很少有人能把每種實現(xiàn)方式的不同講清楚的,今天我們就來探討下它。

1、單例模式的作用

單例模式主要用于解決多次調(diào)用的地方都是用的同一個實例,避免一個應(yīng)用中存在多個該對象的實例,常見的實現(xiàn)手段就是不允許外部創(chuàng)建對象,將構(gòu)造方法私有化,自己提供靜態(tài)方法將對象的實例暴露出去。

2、常見的幾種單例模式的實現(xiàn)

2.1 懶漢、餓漢、變種餓漢(靜態(tài)代碼塊實現(xiàn))模式

這兩種模式在實現(xiàn)上都有一定的缺陷,懶漢模式要實現(xiàn)多線程安全就需要對getInstance()方法上加上synchronize 關(guān)鍵字,效率很低,餓漢模式雖然一開始就創(chuàng)建了實例,但是在類加載的時候就創(chuàng)建了對象,不能實現(xiàn)懶加載。

懶漢:

/**
 * Created by jiangcheng on 2017/9/28.
 */
public class Singleton {

    private static Singleton sInstance;
    // 私有構(gòu)造方法
    private Singleton() {}

    public static synchronized Singleton getInstance () {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

2.2 雙重檢測

由于懶漢模式是對方法進(jìn)行加鎖的,所以當(dāng)多個線程同時訪問該方法時,效率很低,synchronized修飾的同步方法比一般方法要慢很多。

雙重檢測:

public class Singleton {

    private static Singleton sInstance;

    private Singleton() {}

    public static Singleton getInstance () {
        if (sInstance == null) {   // 1
            synchronized (Singleton.class) {
                if (sInstance == null) {  // 2
                    sInstance = new Singleton();
                }
            }
        }

        return sInstance;
    }
}

我們來分析下,雙重檢測的原理,可以看到上面代碼1處在同步代碼塊外多了一層instance為空的判斷。由于單例對象只需要創(chuàng)建一次,如果后面再次調(diào)用getInstance()只需要直接返回單例對象。因此,大部分情況下,調(diào)用getInstance()都不會執(zhí)行到同步代碼塊,從而提高了程序性能。不過還需要考慮一種情況,假如兩個線程A、B,A執(zhí)行了if (instance == null)語句,它會認(rèn)為單例對象沒有創(chuàng)建,此時線程切到B也執(zhí)行了同樣的語句,B也認(rèn)為單例對象沒有創(chuàng)建,然后兩個線程依次執(zhí)行同步代碼塊,并分別創(chuàng)建了一個單例對象。為了解決這個問題,還需要在同步代碼塊中增加if (instance == null)語句,也就是上面看到的代碼2。

  • 這種實現(xiàn)就真的完美么?

由于java平臺的指定優(yōu)化重排后的無序性,會導(dǎo)致初始化Singleton和將對象地址賦給instance字段的順序是不確定的。在某個線程創(chuàng)建單例對象時,在構(gòu)造方法被調(diào)用之前,就為該對象分配了內(nèi)存空間并將對象的字段設(shè)置為默認(rèn)值。此時就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對象可能還沒有初始化。若緊接著另外一個線程來調(diào)用getInstance,取到的就是狀態(tài)不正確的對象,程序就會出錯,雙重檢測就會失效。

不過JDK 1.5后 Java中提供了關(guān)鍵字 volatile。 volatile的一個語義是禁止指令重排序優(yōu)化,他定義的變量在多線程操作中,是對所有線程可見的,也就保證了instance變量被賦值的時候?qū)ο笠呀?jīng)是初始化過的,從而避免了上面說到的問題。

private static volatile Singleton sInstance;    // volatile 關(guān)鍵字的使用

2.3 靜態(tài)內(nèi)部類(推薦)

public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static Singleton sInstance = new Singleton();
    }

    public static Singleton getInstance () {
        return SingletonHolder.sInstance;
    }
}

這種方式利用了Java在類加載的時候只允許一個線程加載,它是在內(nèi)部類里面去創(chuàng)建對象實例。這樣的話,只要應(yīng)用中不使用內(nèi)部類,JVM就不會去加載這個單例類,也就不會創(chuàng)建單例對象,從而實現(xiàn)懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。這種方式比較容易看的懂,也很簡潔,所以推薦改種實現(xiàn)方式。

2.4 枚舉

public enum Singleton {
    instance;
    public void whateverMethod() {
    }
}

由于前面幾種都有兩個共同的缺點:

  1. 需要額外的工作來實現(xiàn)序列化,否則每次反序列化一個序列化的對象時都會創(chuàng)建一個新的實例。
  2. 可以使用反射強(qiáng)行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常)。

而枚舉類很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外,還提供了自動序列化機(jī)制,防止反序列化的時候創(chuàng)建新的對象。因此,《Effective Java》作者推薦使用的方法。不過,在實際工作中,很少看見有人這么寫。

枚舉在Java中是很特殊的存在,可以參考Java枚舉enum及其應(yīng)用

3、總結(jié)

上面幾種單例模式的實現(xiàn)都各有優(yōu)缺點,適用于不同的場景,不過雙重檢測和靜態(tài)內(nèi)部類能解決工作中大部分的問題,枚舉雖然很有特色、很完美,但是工作中用的還是比較少的。我的建議是使用靜態(tài)內(nèi)部類,簡單易懂。

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

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

  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡單、最易理解的設(shè)計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,530評論 4 34
  • 1 場景問題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個應(yīng)用,讀取配置文件的內(nèi)容。 很多應(yīng)用項目,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,966評論 12 68
  • 1.單例模式概述 (1)引言 單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計模式中最基本的一個。本文旨在總結(jié)通過Ja...
    曹豐斌閱讀 3,062評論 6 47
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,623評論 1 8
  • 小魚兒是一個心比天高的女孩兒,但是命途多舛。因為家里窮,媽媽長期臥病在床,所以小魚兒早早的挑起了家里的重?fù)?dān)。為了減...
    凡愛閱讀 304評論 0 0

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