java單例模式

①餓漢式

public class Hungry {
    private Hungry(){}
    private static final Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }

    public static void main(String[] args) {
        Hungry hungry = Hungry.getInstance();
        System.out.println(hungry);
    }
}

一上來(lái)就把對(duì)象new出來(lái)晕拆,并用static final修飾昌妹,通過getInstance方法返回該對(duì)象
這種方式缺點(diǎn)也很明顯寝衫,不管用沒用這個(gè)對(duì)象柬批,都先new出來(lái)啸澡,浪費(fèi)了內(nèi)存

②懶漢式

public class Lazy {
    private Lazy(){
        System.out.println(Thread.currentThread().getName()+"調(diào)用了構(gòu)造函數(shù)");
    }
    private static Lazy lazy;
    public static Lazy getInstance(){
        if(lazy == null){
            lazy = new Lazy();
        }
        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Lazy.getInstance();
            }).start();
        }
    }
}
Thread-0調(diào)用了構(gòu)造函數(shù)
Thread-5調(diào)用了構(gòu)造函數(shù)
Thread-3調(diào)用了構(gòu)造函數(shù)
Thread-9調(diào)用了構(gòu)造函數(shù)
Thread-1調(diào)用了構(gòu)造函數(shù)
Thread-2調(diào)用了構(gòu)造函數(shù)

這種方式相對(duì)于懶漢式有所改進(jìn),在類中只聲明對(duì)象萝快,不實(shí)例化锻霎;調(diào)用getInstance方法時(shí),對(duì)象為null則實(shí)例化揪漩,否則直接返回旋恼。按理說(shuō)只有第一次調(diào)用getInstance方法時(shí)會(huì)調(diào)用構(gòu)造函數(shù),new對(duì)象奄容,以后都直接return冰更。

單線程下確實(shí)沒問題,但是多線程時(shí)昂勒,由運(yùn)行結(jié)果可知蜀细,構(gòu)造函數(shù)被多次調(diào)用,顯然不符合單例模式戈盈。問題就出在多個(gè)線程同時(shí)調(diào)用了getInstance方法奠衔,解決方法就是通過synchronized加鎖同步。

public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
    }

    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}
Thread-0調(diào)用了構(gòu)造函數(shù)

這時(shí)只有第一個(gè)線程調(diào)用了構(gòu)造函數(shù)塘娶,符合預(yù)期的結(jié)果归斤。

但是事實(shí)上還要考慮另一個(gè)問題:當(dāng)new一個(gè)對(duì)象時(shí),并不是一個(gè)原子操作刁岸,可以分為三個(gè)步驟:①為對(duì)象分配內(nèi)存空間脏里,②實(shí)例化對(duì)象,③向?qū)ο蟮囊弥赶蛟搩?nèi)存空間虹曙。正常情況下按照①②③順序執(zhí)行是沒問題的迫横,但是cpu可能會(huì)指令重排番舆,執(zhí)行順序變?yōu)棰佗邰冢@時(shí)如果一個(gè)線程執(zhí)行到③(即對(duì)象引用指向了內(nèi)存空間矾踱,不為null恨狈,但是由于對(duì)象還沒有實(shí)例化,這塊內(nèi)存什么都沒有)介返,另外一個(gè)線程同時(shí)調(diào)用了getInstance方法拴事,判斷l(xiāng)azy不為null沃斤,直接返回了lazy圣蝎,就出問題了。解決方法就是使用volatile關(guān)鍵字禁止指令重排衡瓶。

public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

以上就是雙重檢測(cè)鎖DCL

為什么要判斷兩次lazy == null徘公?
假設(shè)第一個(gè)線程進(jìn)入同步塊new對(duì)象的過程中,第二個(gè)線程同時(shí)調(diào)用了getInstance方法哮针,因?yàn)閷?duì)象還沒有被實(shí)例化关面,外面的if不成立,所以也將會(huì)執(zhí)行同步塊中的代碼十厢,但是目前處于阻塞狀態(tài)等太。等到第一個(gè)線程創(chuàng)建完對(duì)象,第二個(gè)線程進(jìn)入同步塊蛮放,就會(huì)判斷里面的if不成立(第一個(gè)線程已經(jīng)實(shí)例化了對(duì)象)缩抡,就不會(huì)再new了,如果沒有里面的if包颁,第二個(gè)線程就會(huì)又new一個(gè)瞻想,單例模式就不成立了。

以上單例模式娩嚼,由于java反射機(jī)制的存在蘑险,都是不安全的,如②懶漢式岳悟,可以通過如下反射破環(huán)單例模式:

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Lazy.getInstance();
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();

    }
}
main調(diào)用了構(gòu)造函數(shù)
main調(diào)用了構(gòu)造函數(shù)

上述代碼中佃迄,先通過getInstance方法正常創(chuàng)建對(duì)象,然后又通過反射newInstance創(chuàng)建對(duì)象贵少。由運(yùn)行結(jié)果可知呵俏,兩次創(chuàng)建對(duì)象都調(diào)用了構(gòu)造函數(shù),因?yàn)榉瓷渫ㄟ^setAccessible(true)破壞了構(gòu)造函數(shù)的私有性春瞬,使得單例模式被破壞柴信。

如果加一個(gè)字段驗(yàn)證呢?

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Lazy {
    private static boolean flag = false;
    private Lazy() {
        if(!flag){
            flag = true;
            System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
        }else{
            throw new RuntimeException("不要試圖通過反射破壞單例宽气!");
        }
       
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Lazy.getInstance();
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();

    }
}
main調(diào)用了構(gòu)造函數(shù)
Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at Lazy.main(Lazy.java:45)
Caused by: java.lang.RuntimeException: 不要試圖通過反射破壞單例随常!
        at Lazy.<init>(Lazy.java:13)
        ... 5 more

這次加了一個(gè)flag字段潜沦,用在構(gòu)造函數(shù)里,判斷是否是第一次調(diào)用绪氛,第二次再調(diào)用時(shí)就會(huì)拋出異常唆鸡,這樣就無(wú)法通過反射無(wú)限制地創(chuàng)建對(duì)象了。

但是仍然是存在不安全性的枣察,以上結(jié)論是在不知道這個(gè)字段的情況下實(shí)現(xiàn)的争占。所謂道高一尺,魔高一丈序目,如果這個(gè)驗(yàn)證字段被破解了臂痕,就可以通過反射重新設(shè)置它的值。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Lazy {
    private static boolean flag = false;

    private Lazy() {
        if (!flag) {
            flag = true;
            System.out.println(Thread.currentThread().getName() + "調(diào)用了構(gòu)造函數(shù)");
        } else {
            throw new RuntimeException("不要試圖通過反射破壞單例猿涨!");
        }

    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }

        return lazy;
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchFieldException {
        // 正常創(chuàng)建第一個(gè)對(duì)象
        Lazy lazy1 = Lazy.getInstance();

        // 反射修改flag
        Field flag = Lazy.class.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(lazy1, false);

        // 通過反射再次創(chuàng)建對(duì)象
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();

    }
}
main調(diào)用了構(gòu)造函數(shù)
main調(diào)用了構(gòu)造函數(shù)

③靜態(tài)內(nèi)部類

public class External {
    private External(){}
    public static class Internal{
        private static final External EXTERNAL = new External();
    }
    public static External getInstance(){
        return Internal.EXTERNAL;
    }
}

將類的實(shí)例作為靜態(tài)內(nèi)部類的成員屬性握童,再通過getInstance方法獲得

④枚舉

以上三種方法都存在不安全性,那么怎么才能保證安全呢叛赚,查看newInstance源碼澡绩,直接就能看到這段代碼:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

不能通過反射創(chuàng)建枚舉對(duì)象!
枚舉類型其實(shí)是自帶單例模式

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingle {

    INSTANCE;

    public static EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        EnumSingle instance1 = EnumSingle.getInstance();
        
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingle instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);

    }
}
Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082)
        at java.lang.Class.getDeclaredConstructor(Class.java:2178)
        at EnumSingle.main(EnumSingle.java:17)

好像哪里不對(duì)俺附?出錯(cuò)是必然的肥卡,但是錯(cuò)誤信息應(yīng)該是上面說(shuō)的Cannot reflectively create enum objects,而不是這個(gè)事镣。

NoSuchMethodException步鉴,沒有該方法,顯然是構(gòu)造函數(shù)不對(duì)蛮浑,通過反編譯class文件唠叛,得知構(gòu)造函數(shù)有兩個(gè)參數(shù),分別為Stringint

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingle {

    INSTANCE;

    public static EnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        EnumSingle instance1 = EnumSingle.getInstance();

        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingle instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);

    }
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at EnumSingle.main(EnumSingle.java:19)

這才是想要的異常
得出結(jié)論:通過枚舉實(shí)現(xiàn)的單例模式是安全的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沮稚,一起剝皮案震驚了整個(gè)濱河市艺沼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蕴掏,老刑警劉巖障般,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異盛杰,居然都是意外死亡挽荡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門即供,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)定拟,“玉大人,你說(shuō)我怎么就攤上這事逗嫡∏嘧裕” “怎么了株依?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)延窜。 經(jīng)常有香客問我恋腕,道長(zhǎng),這世上最難降的妖魔是什么逆瑞? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任荠藤,我火速辦了婚禮,結(jié)果婚禮上获高,老公的妹妹穿的比我還像新娘哈肖。我一直安慰自己,他們只是感情好谋减,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布牡彻。 她就那樣靜靜地躺著扫沼,像睡著了一般出爹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缎除,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天严就,我揣著相機(jī)與錄音,去河邊找鬼器罐。 笑死梢为,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轰坊。 我是一名探鬼主播铸董,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肴沫!你這毒婦竟也來(lái)了粟害?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤颤芬,失蹤者是張志新(化名)和其女友劉穎悲幅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體站蝠,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汰具,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了菱魔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片留荔。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖澜倦,靈堂內(nèi)的尸體忽然破棺而出聚蝶,到底是詐尸還是另有隱情拔疚,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布既荚,位于F島的核電站稚失,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏恰聘。R本人自食惡果不足惜句各,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晴叨。 院中可真熱鬧凿宾,春花似錦、人聲如沸兼蕊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)孙技。三九已至产禾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間牵啦,已是汗流浹背亚情。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哈雏,地道東北人楞件。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裳瘪,于是被迫代替她去往敵國(guó)和親土浸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355