單例模式(Singleton)

單例模式.png

定義:一個(gè)類只有一個(gè)實(shí)例,并且該類可以自行創(chuàng)建這個(gè)實(shí)例的一種模式。

優(yōu)點(diǎn):

  • 減少內(nèi)存資源
  • 保證數(shù)據(jù)內(nèi)容一致性

缺點(diǎn):

  • 單例模式一般沒(méi)有接口,擴(kuò)展困難,如果要擴(kuò)展需要修改原來(lái)的代碼,違反開(kāi)閉原則
  • 并發(fā)測(cè)試,單例不利于代碼調(diào)試。

應(yīng)用場(chǎng)景:

  • 需要頻繁創(chuàng)建的一些類,可以考慮使用單例
  • 某類只要求生成一個(gè)對(duì)象的時(shí)候,如一個(gè)班中的班長(zhǎng)、每個(gè)人的身份證號(hào)等。
  • 某些類創(chuàng)建實(shí)例時(shí)占用資源較多,或?qū)嵗臅r(shí)較長(zhǎng),且經(jīng)常使用。
  • 某類需要頻繁實(shí)例化,而創(chuàng)建的對(duì)象又頻繁被銷毀的時(shí)候,如多線程的線程池、網(wǎng)絡(luò)連接池等。
  • 頻繁訪問(wèn)數(shù)據(jù)庫(kù)或文件的對(duì)象。
  • 對(duì)于一些控制硬件級(jí)別的操作,或者從系統(tǒng)上來(lái)講應(yīng)當(dāng)是單一控制邏輯的操作,如果有多個(gè)實(shí)例,則系統(tǒng)會(huì)完全亂套。
  • 當(dāng)對(duì)象需要被共享的場(chǎng)合。由于單例模式只允許創(chuàng)建一個(gè)對(duì)象,共享該對(duì)象可以節(jié)省內(nèi)存,并加快對(duì)象訪問(wèn)速度。如 Web 中的配置對(duì)象、數(shù)據(jù)庫(kù)的連接池等。

如何手寫(xiě)一個(gè)單例?

三步走

  • 構(gòu)造器私有化

  • 自行創(chuàng)建,用靜態(tài)變量保存

  • 提供獲取方法(靜態(tài)方法)

上代碼。。。。

/**
 * 餓漢式
 * 優(yōu)點(diǎn):效率高,線程安全
 * 缺點(diǎn):占用內(nèi)存
 */
package com.yang.singleton;

public class Singleton01 {
    //自行創(chuàng)建,用靜態(tài)變量保存
    private static Singleton01 instance = new Singleton01();
    //構(gòu)造器私有化
    private Singleton01() {
    }
    //向外提供獲取方法
    public static Singleton01 getInstance() {
        return instance;
    }
}

一分鐘解決戰(zhàn)斗。。。

測(cè)試一下

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
        }, "線程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
        }, "線程2").start();
    }
}

線程1: com.yang.singleton.Singleton01@6218fb2e
線程2: com.yang.singleton.Singleton01@6218fb2e

測(cè)試結(jié)果沒(méi)問(wèn)題,類加載的時(shí)候就已經(jīng)初始化創(chuàng)建實(shí)例 ,所以不存在訪問(wèn)線程安全問(wèn)題,但是有一個(gè)缺點(diǎn),如果這個(gè)類沒(méi)有被使用,就會(huì)一直占用內(nèi)存資源,造成內(nèi)存浪費(fèi)

優(yōu)化一下。。。

/**
 * 懶漢式
 * 優(yōu)點(diǎn):調(diào)用時(shí)才創(chuàng)建實(shí)例
 * 缺點(diǎn):線程不安全
 */
package com.yang.singleton;

public class Singleton02 {

    private static Singleton02 instance = null;

    private Singleton02() {
    }
    //修改成需要使用時(shí),創(chuàng)建對(duì)象,節(jié)省內(nèi)存空間
    public static Singleton02 getInstance() {
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}

測(cè)試一下。。。

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton02.getInstance());
        }, "線程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton02.getInstance());
        }, "線程2").start();
    }
}
線程1: com.yang.singleton.Singleton02@6dd2a02f
線程2: com.yang.singleton.Singleton02@4920b564

結(jié)果創(chuàng)建了兩個(gè)對(duì)象。

思考后認(rèn)識(shí)到,修改為調(diào)用方法的時(shí)候創(chuàng)建對(duì)象,但是這個(gè)時(shí)候如果兩個(gè)線程同時(shí)調(diào)用getInstance()方法,此時(shí)對(duì)于兩個(gè)線程來(lái)說(shuō)instance == null都為true,兩個(gè)線程都會(huì)創(chuàng)建一個(gè)新的對(duì)象,就會(huì)有線程安全問(wèn)題。

知道問(wèn)題所在,再稍作修改。。。

/**
 * 懶漢式
 * 優(yōu)點(diǎn):線程安全
 * 缺點(diǎn):性能差
 */
package com.yang.singleton;

public class Singleton02 {

    private static Singleton02 instance = null;

    private Singleton02() {
    }

    public static synchronized Singleton02 getInstance() {
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}


測(cè)試結(jié)果

線程1: com.yang.singleton.Singleton02@6dd2a02f
線程2: com.yang.singleton.Singleton02@6dd2a02f

getInstance()方法上加 synchronized關(guān)鍵字(為方法加鎖),好像可以了。

但是只有在第一次實(shí)例化的時(shí)候才有必要加鎖保證單例,感覺(jué)沒(méi)有必要每次調(diào)用getInstance()方法都讓它排隊(duì)等待,安全性保證了,但是影響了程序的性能。

將鎖的粒度調(diào)整一下

/**
 * DCL(Double Check Lock)
 * 優(yōu)點(diǎn):線程安全,調(diào)用時(shí)創(chuàng)建實(shí)例
 * 缺點(diǎn):復(fù)雜,可讀性差
 */
package com.yang.singleton;

public class Singleton02 {

    private volatile static Singleton02 instance = null;

    private Singleton02() {
    }

    public static Singleton02 getInstance() {
        //判斷是否需要阻塞
        if (instance == null) {
            //線程1,線程2。。。在這里等待。。
            synchronized (Singleton02.class) {
                //判斷是否別的線程已經(jīng)創(chuàng)建了實(shí)例
                if (instance == null) {
                    instance = new Singleton02();
                }
            }
        }
        return instance;
    }
}
線程1: com.yang.singleton.Singleton02@6dd2a02f
線程2: com.yang.singleton.Singleton02@6dd2a02f

我們真正想要加鎖的其實(shí)只是創(chuàng)建實(shí)例這一步,在多個(gè)線程進(jìn)入方法時(shí),先判斷instance == null,如果為true將接下來(lái)的動(dòng)作加鎖,否則直接返回,極大的提高了效率,第二個(gè)instance == null判斷是否其它線程已經(jīng)創(chuàng)建了實(shí)例,有則直接返回。

  • 第一個(gè) instance == null判斷是否需要阻塞
  • 第二個(gè) instance == null判斷是否別的線程已經(jīng)創(chuàng)建了實(shí)例

細(xì)心的人會(huì)發(fā)現(xiàn)不僅僅是加了兩層判空,在變量上還加了volatile關(guān)鍵字

volatile作用:

  • 保證變量可見(jiàn)性
  • 禁止指令重排序

簡(jiǎn)單解釋一下,想要詳細(xì)了解可自行百度。

可見(jiàn)性就如字面意思,當(dāng)一個(gè)線程改變了變量的值,其它持有該變量的線程能夠立即感知到,避免了臟讀的現(xiàn)象。

指令重排,Java代碼最終會(huì)被轉(zhuǎn)化為字節(jié)碼,JVM通過(guò)解釋字節(jié)碼將其翻譯成一條條對(duì)應(yīng)的機(jī)器指令。

創(chuàng)建對(duì)象的過(guò)程大致分為以下幾步:

  1. 申請(qǐng)分配內(nèi)存空間
  2. 初始化對(duì)象
  3. 設(shè)置對(duì)象(instance )指向剛剛分配的內(nèi)存地址

正常執(zhí)行順序應(yīng)該是1-->2-->3,但是當(dāng)2初始化對(duì)象的時(shí)間過(guò)長(zhǎng)時(shí),編譯器會(huì)交換2和3的順序,

導(dǎo)致執(zhí)行最終執(zhí)行的順序?yàn)?-->3-->2,當(dāng)執(zhí)行到3的時(shí)候另外的線程判斷instance == null不成立(此時(shí)instance 不為空,已經(jīng)指向內(nèi)存地址),直接返回沒(méi)有初始化的instance,造成NPE

加上volatile可以禁止2和3交換順序,按順序執(zhí)行,避免NPE

最終得到了一個(gè)線程安全,省內(nèi)存的單例

但是防這防那的,未免太過(guò)復(fù)雜。有鎖就會(huì)有性能問(wèn)題

有沒(méi)有不加鎖的方法?

重新整理一下思路。

以餓漢式為始,后面衍生出來(lái)的一系列問(wèn)題都是由于想要將創(chuàng)建實(shí)例的動(dòng)作滯后,達(dá)到節(jié)省內(nèi)存的目的,那么有沒(méi)有別的方法可以讓我們?cè)谡{(diào)用的時(shí)候創(chuàng)建。

可以利用靜態(tài)內(nèi)部類

調(diào)用的時(shí)候加載

上代碼。。。

/**
 * 靜態(tài)內(nèi)部類
 * 優(yōu)點(diǎn):線程安全,調(diào)用時(shí)創(chuàng)建實(shí)例
 */
package com.yang.singleton;

public class Singleton03 {

    private Singleton03() {
    }

    public static Singleton03 getInstance() {
        return InnerSingleton.INSTANCE;
    }

    private static class InnerSingleton {
        private static final Singleton03 INSTANCE = new Singleton03();
    }
}
線程2: com.yang.singleton.User@4920b564
線程1: com.yang.singleton.User@4920b564

至此,單例可以告一段落

---------------------------------------------------無(wú)敵分割線---------------------------------------------
下面補(bǔ)充幾種網(wǎng)上搜集來(lái)的單例寫(xiě)法

枚舉

/**
 * 枚舉
 * 優(yōu)點(diǎn):線程安全,防止反序列化
 */
package com.yang.singleton;

public class User {
    //私有化構(gòu)造函數(shù)
    private User() {
    }
    //定義一個(gè)靜態(tài)枚舉類
    static enum SingletonEnum {
        //創(chuàng)建一個(gè)枚舉對(duì)象,該對(duì)象天生為單例
        INSTANCE;
        private User user;
        //私有化枚舉的構(gòu)造函數(shù)
        private SingletonEnum() {
            user = new User();
        }
        public User getInstnce() {
            return user;
        }
    }
    //對(duì)外暴露一個(gè)獲取User對(duì)象的靜態(tài)方法
    public static User getInstance() {
        return SingletonEnum.INSTANCE.getInstnce();
    }
}
線程2: com.yang.singleton.User@2fec69f9
線程1: com.yang.singleton.User@2fec69f9

容器式單例

/**
 * 容器式單例
 * 實(shí)現(xiàn)思路:用一個(gè)Map保存對(duì)象,當(dāng)需要使用的時(shí)候去map里拿,如果有,則直接取出來(lái),沒(méi)有則利用返回創(chuàng)建一個(gè),放到map里,然后再返回出去。(相當(dāng)于一個(gè)簡(jiǎn)單的Spring管理容器)
 */
package com.yang.singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton {

    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
        Object instance2 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
        System.out.println("instance1: " + instance1);
        System.out.println("instance2: " + instance2);

    }
}
instance1: com.yang.singleton.TestPojo@61bbe9ba
instance2: com.yang.singleton.TestPojo@61bbe9ba

但是上述示例存在線程安全問(wèn)題

看看在多線程環(huán)境下測(cè)試結(jié)果

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
        }, "線程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
        }, "線程2").start();
    }
}
線程1: com.yang.singleton.TestPojo@6dd2a02f
線程2: com.yang.singleton.TestPojo@4920b564

簡(jiǎn)單修改一下

package com.yang.singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton {

    private ContainerSingleton() {
    }

    private volatile static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        Object instance = null;
        //如果容器里沒(méi)有,判斷是否需要加鎖
        if (!ioc.containsKey(className)) {
            synchronized (ContainerSingleton.class) {
                if (!ioc.containsKey(className)) {
                    try {
                        instance = Class.forName(className).newInstance();
                        ioc.put(className, instance);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return instance;
                } else {
                    return ioc.get(className);
                }
            }
        } else {
            return ioc.get(className);
        }
    }
}
線程1: com.yang.singleton.TestPojo@43d83830
線程2: com.yang.singleton.TestPojo@43d83830

CAS單例

/**
 * CAS單例
 * 思路:用AtomicReference包裝保證原子性,然后利用無(wú)鎖機(jī)制,循環(huán)去判斷是否有現(xiàn)成的對(duì)象,有就返回,沒(méi)有就創(chuàng)建,但后替INSTANCE
 */
package com.yang.singleton;

import java.util.concurrent.atomic.AtomicReference;

public class CASSingleton {
    private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();

    private CASSingleton() {
    }

    public static CASSingleton getInstance() {
        for (; ; ) {
            CASSingleton singleton = INSTANCE.get();
            if (singleton != null) { 
                return singleton;
            }
            singleton = new CASSingleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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