<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          設(shè)計(jì)模式之單例模式(Singleton)

          共 15772字,需瀏覽 32分鐘

           ·

          2021-03-31 20:45

          定義

          一個類只有一個實(shí)例,它自己負(fù)責(zé)創(chuàng)建自己的對象,這個類提供了一種訪問其唯一對象的方式,可以直接訪問,不需要實(shí)例化該類的對象

          單例模式有以下三個特點(diǎn):

          1、單例類只能有一個實(shí)例
          2、單例類必須自己創(chuàng)建自己的唯一實(shí)例
          3、單例類必須給所有其他對象提供這一實(shí)例

          如果要實(shí)現(xiàn)這三點(diǎn),需要滿足如下三個要求:

          1、單例類中含有一個該類的私有的靜態(tài)實(shí)例
          2、單例類只提供私有的構(gòu)造函數(shù)
          3、單例類提供一個公有的靜態(tài)的函數(shù)用于獲取它本身的私有靜態(tài)實(shí)例

          單例模式UML圖
          二、單例模式示例
          1、單例模式分類

          單例模式根據(jù)實(shí)例的創(chuàng)建時機(jī)來劃分可分為:餓漢式和懶漢式。

          餓漢式:應(yīng)用剛啟動的時候,不管外部有沒有調(diào)用該類的實(shí)例方法,該類的實(shí)例就已經(jīng)創(chuàng)建好了。

          懶漢式:應(yīng)用剛啟動的時候,并不創(chuàng)建實(shí)例,等到第一次被使用時,才創(chuàng)建該類的實(shí)例。

          2、舉例說明

          (1)、餓漢式(線程安全,可用)

          1 public class Singleton {
          2    // 私有的靜態(tài)實(shí)例
          3    private final static Singleton instance = new Singleton();
          4
          5    // 私有的構(gòu)造函數(shù),防止在該類外部通過new創(chuàng)建實(shí)例
          6    private Singleton(){
          7        System.out.println("創(chuàng)建Singleton實(shí)例!");
          8    }
          9
          10    // 公有的靜態(tài)的函數(shù),用于獲取實(shí)例
          11    public static Singleton getInstance(){
          12        return instance;
          13    }
          14}
          15
          16public class Test {
          17    public static void main(String[] args) {
          18        Singleton instance1 = Singleton.getInstance();
          19        System.out.println(instance1);
          20
          21        Singleton instance2 = Singleton.getInstance();
          22        System.out.println(instance2);
          23
          24        Singleton instance3 = Singleton.getInstance();
          25        System.out.println(instance3);
          26    }
          27}

          程序運(yùn)行結(jié)果:

          創(chuàng)建Singleton實(shí)例!
          com.zxj.test.Singleton@154617c
          com.zxj.test.Singleton@154617c
          com.zxj.test.Singleton@154617c

          觀察程序運(yùn)行結(jié)果:只創(chuàng)建了一個實(shí)例,三次調(diào)用獲取到的實(shí)例都是同一個。

          餓漢式優(yōu)點(diǎn):
          在類加載的時候就完成了實(shí)例化,避免了多線程的同步問題。

          餓漢式缺點(diǎn):
          在外部沒有使用到該類的時候,該類的實(shí)例就創(chuàng)建了,若該類的實(shí)例的創(chuàng)建比較消耗系統(tǒng)資源,并且外部一直沒有調(diào)用該實(shí)例,那么這部分的系統(tǒng)資源的消耗是沒有意義的。

          (2)、懶漢式(線程不安全,不可用)
          (a).單線程環(huán)境下,是沒有問題的:

          1public class Singleton {
          2    private static Singleton instance = null;
          3
          4    private Singleton(){
          5        System.out.println("創(chuàng)建Singleton實(shí)例!");
          6    }
          7
          8    public static Singleton getInstance(){
          9        if(instance == null){
          10            instance = new Singleton();
          11        }
          12        return instance;
          13    }
          14}
          15
          16public class Test {
          17    public static void main(String[] args) {
          18        Singleton instance1 = Singleton.getInstance();
          19        System.out.println(instance1);
          20
          21        Singleton instance2 = Singleton.getInstance();
          22        System.out.println(instance2);
          23
          24        Singleton instance3 = Singleton.getInstance();
          25        System.out.println(instance3);
          26    }
          27}

          程序運(yùn)行結(jié)果:

          創(chuàng)建Singleton實(shí)例!
          com.zxj.test2.Singleton@154617c
          com.zxj.test2.Singleton@154617c
          com.zxj.test2.Singleton@154617c

          (b).多線程環(huán)境下,單例模式失效,程序中有多個實(shí)例:

          1public class Test {
          2    public static void main(String[] args) {
          3        for(int i = 0; i < 10; i++){
          4            new Thread(){
          5                @Override
          6                public void run(){
          7                    Singleton instance = Singleton.getInstance();
          8                    System.out.println(instance);
          9                }
          10            }.start();
          11        }
          12    }
          13}

          程序運(yùn)行結(jié)果:

          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          創(chuàng)建Singleton實(shí)例!
          com.zxj.test2.Singleton@1329a4
          com.zxj.test2.Singleton@10ada9c
          com.zxj.test2.Singleton@16268e5
          com.zxj.test2.Singleton@95e4d4
          com.zxj.test2.Singleton@2191b2
          com.zxj.test2.Singleton@173a9e8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1122c7b
          com.zxj.test2.Singleton@16ab049
          com.zxj.test2.Singleton@15cc318

          我們來分析一下,為什么在多線程環(huán)境下,懶漢式單例模式會失效?
          假設(shè)有兩個線程,線程A和線程B,線程A執(zhí)行到第10行,但是并沒有執(zhí)行這一行,這個時候,線程B執(zhí)行到第9行,線程B判斷結(jié)果是true,線程B執(zhí)行第10行,new了一個對象,此時,線程A,這就造成執(zhí)行個對象被new了兩次。

          假設(shè)有兩個線程,線程A和線程B,線程A先得到CPU的執(zhí)行權(quán),線程A執(zhí)行到第9行 ,由于instance之前并沒有實(shí)例化,所以線程A判斷結(jié)果為true,線程A還沒有來得及執(zhí)行第10行,CPU執(zhí)行權(quán)就被線程B搶去了,線程B執(zhí)行到第9行,由于instance之前并沒有實(shí)例化,所以線程B判斷結(jié)果為true,此時線程B new了一個實(shí)例。之后CPU執(zhí)行權(quán)分給線程A,線程A接著執(zhí)行第10行實(shí)例的創(chuàng)建。由此看到線程A和線程B分別創(chuàng)建了一個實(shí)例(存在2個實(shí)例了),這就導(dǎo)致了單例的失效。

          那么如何保證懶漢式在多線程環(huán)境仍然保證只有一個單例呢?當(dāng)然是在創(chuàng)建實(shí)例時進(jìn)行同步控制。

          (3)、懶漢式(線程安全,可用)

          1public class Singleton {
          2    private static Singleton instance = null;
          3
          4    private Singleton(){
          5        System.out.println("創(chuàng)建Singleton實(shí)例!");
          6    }
          7
          8    // 對整個獲取實(shí)例的方法進(jìn)行同步
          9    public static synchronized Singleton getInstance(){
          10        if(instance == null){
          11            instance = new Singleton();
          12        }
          13        return instance;
          14    }
          15}

          這種寫法,對獲取實(shí)例的整個方法用synchronized關(guān)鍵字進(jìn)行方法同步,保證了同一時刻只能有一個線程能夠訪問并獲得實(shí)例。

          但是缺點(diǎn)也很明顯,這里鎖住的是整個方法,鎖的粒度太大,造成效率低下,那應(yīng)該怎么辦呢?減小鎖的粒度,只把創(chuàng)建實(shí)例這塊代碼上鎖,見下面的雙重校驗(yàn)鎖方式。

          (4)、雙重校驗(yàn)鎖(DCL,double-checked locking,線程安全,可用)

          雙重校驗(yàn)鎖,雙重校驗(yàn)說的是兩個空值判斷,鎖說的是synchronized。

          (a).雙重校驗(yàn)鎖代碼

          1public class Singleton {
          2    private static Singleton instance = null;
          3
          4    private Singleton(){
          5        System.out.println("創(chuàng)建Singleton實(shí)例!");
          6    }
          7    // 對必要的代碼塊(創(chuàng)建實(shí)例的代碼塊)進(jìn)行同步
          8    public static Singleton getInstance(){
          9        if (instance == null) {
          10            synchronized (Singleton.class{
          11                if (instance == null) {
          12                    instance = new Singleton();
          13                }
          14            }
          15        }
          16        return instance;
          17    }
          18}
          1public class Test {
          2    public static void main(String[] args) {
          3        for(int i = 0; i < 10; i++){
          4            new Thread(){
          5                @Override
          6                public void run(){
          7                    Singleton instance = Singleton.getInstance();
          8                    System.out.println(instance);
          9                }
          10            }.start();
          11        }
          12    }
          13}

          程序運(yùn)行結(jié)果:

          創(chuàng)建Singleton實(shí)例!
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8
          com.zxj.test2.Singleton@1a1b9f8

          (b).關(guān)于雙重校驗(yàn)鎖的兩個疑問

          從程序運(yùn)行結(jié)果可以看到,程序中只有一個實(shí)例。關(guān)于雙重校驗(yàn)鎖,小伙伴們一定會有一些疑問,下面我們就來共同一一解答。

          問題1:既然有了一次空值判斷,為什么還要再加一層空值判斷呢?

          答案:這主要涉及線程安全的問題

          這個問題的意思,就是在第9行進(jìn)行實(shí)例非空判斷之后,進(jìn)入synchronized代碼塊之后就不必再進(jìn)行一次非空判斷了,我們來看一下,如果這樣做的話,會產(chǎn)生什么問題?

          假設(shè)有兩個線程,線程A和線程B,線程A先得到CPU的執(zhí)行權(quán),在執(zhí)行到第9行時,由于之前沒有實(shí)例化,所以線程A判斷結(jié)果為true,然后線程A獲得鎖進(jìn)入synchronized代碼塊里面。

          此時線程B爭搶到CPU的執(zhí)行權(quán),并執(zhí)行到第9行,此時線程A還沒有執(zhí)行實(shí)例化動作,所以此時線程B判斷為true,線程B想進(jìn)入同步代碼塊,但是發(fā)現(xiàn)鎖還在線程A手里,所以B只能在同步代碼塊外面等待。

          此時線程A得回CPU執(zhí)行權(quán),執(zhí)行實(shí)例化動作并返回該實(shí)例,然后釋放鎖。

          線程A釋放了鎖,線程B就獲得了鎖,若此時不進(jìn)行第二次非空判斷,會導(dǎo)致線程B也實(shí)例化創(chuàng)建一個實(shí)例,然后返回自己創(chuàng)建的實(shí)例,這就導(dǎo)致了2個線程訪問創(chuàng)建了2個實(shí)例,導(dǎo)致單例失效。

          若進(jìn)行第二次非空判斷,線程B發(fā)現(xiàn)線程A已經(jīng)創(chuàng)建了實(shí)例,則直接返回線程A創(chuàng)建的實(shí)例,這樣就避免了單例的失效。

          問題2:即便將第9行的空值判斷去掉,在多線程環(huán)境下單例模式仍然有效,那為什么多次一舉加上第9行代碼呢?

          答案:這主要涉及多線程下的效率問題

          使用synchronized關(guān)鍵字進(jìn)行同步,意味著同一時刻只能有一個線程執(zhí)行同步塊里面的代碼,還要涉及到鎖的爭奪、釋放等問題,是很消耗資源的。如果我們不加第9行,即不在進(jìn)入同步塊之前進(jìn)行非空判斷,如果之前已經(jīng)有線程創(chuàng)建了該類的實(shí)例了,那每次訪問獲取實(shí)例的方法都會進(jìn)入同步塊,這會非常的耗費(fèi)性能如果在進(jìn)入同步塊之前加上非空判斷,如果之前已經(jīng)有線程創(chuàng)建了該類的實(shí)例了,那就不必進(jìn)入同步塊了,直接返回之前創(chuàng)建的實(shí)例即可,減少synchronized操作次數(shù),從而提高 程序性能。

          (c).DCL失效問題,以及解決方案

          在第12行,instance = new Singleton();,其實(shí)這行代碼在JVM里面的執(zhí)行分三步:
          1.在堆內(nèi)存中分配內(nèi)存空間給這個對象。
          2.初始化這個對象。
          3.設(shè)置instance指向剛分配的內(nèi)存空間。

          由于jvm的指令重排序功能,可能在2還沒執(zhí)行時就先執(zhí)行了3,此時第12行就變成:
          1.在堆內(nèi)存中分配內(nèi)存空間給這個對象。
          2.設(shè)置instance指向剛分配的內(nèi)存空間。
          3.初始化這個對象。

          假設(shè)有兩個線程,線程A和線程B,并且第12行代碼發(fā)生指令重排序。

          線程A先獲取到CPU執(zhí)行權(quán),并執(zhí)行到第9行。

          此時線程B爭搶到CPU執(zhí)行權(quán),并執(zhí)行完第12行的第2個步驟。

          此時線程A奪回CPU執(zhí)行權(quán),由于線程B執(zhí)行第12行的第2個步驟,instance已經(jīng)非空了,它會被線程A直接拿來用,這樣的話,就會出現(xiàn)異常,因?yàn)閕nstance只是一個引用,它指向的對象還沒有實(shí)例化呢,自然會出現(xiàn)問題了。這個就是著名的DCL失效問題

          解決DCL失效問題,有兩種方案,一是使用volatile關(guān)鍵字禁止指令重排序,二是使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式。

          使用volatile關(guān)鍵字禁止指令重排序:

          1public class Singleton {
          2    private volatile static Singleton instance = null;
          3
          4    private Singleton(){
          5        System.out.println("創(chuàng)建Singleton實(shí)例!");
          6    }
          7    // 對必要的代碼塊(創(chuàng)建實(shí)例的代碼塊)進(jìn)行同步
          8    public static Singleton getInstance(){
          9        if (instance == null) {
          10            synchronized (Singleton.class{
          11                if (instance == null) {
          12                    instance = new Singleton();
          13                }
          14            }
          15        }
          16        return instance;
          17    }
          18}

          (5)、靜態(tài)內(nèi)部類 (可用,推薦)

          1public class Singleton {
          2    private Singleton() {}
          3
          4    private static class SingletonInstance {
          5        private static final Singleton instance = new Singleton();
          6    }
          7    // 靜態(tài)內(nèi)部類
          8    public static Singleton getInstance() {
          9        return SingletonInstance.instance;
          10    }
          11
          12}

          使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式,是在實(shí)際開發(fā)中最推薦的寫法。

          問題1:靜態(tài)內(nèi)部類,如何保證延遲加載?
          當(dāng)Singleton類第一次被加載時并不會立即實(shí)例化,而是當(dāng)getInstance()方法第一次被調(diào)用時,才會去加載SingletonInstance類,從而完成Singleton的實(shí)例化。

          問題2:靜態(tài)內(nèi)部類,如何保證程序中只有一個實(shí)例?
          因?yàn)轭惖撵o態(tài)屬性只會在第一次加載類的時候初始化,也就保證了SingletonInstance中的對象只會被實(shí)例化一次。

          問題3:如何保證靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例模式在多線程環(huán)境是生效的?
          有兩點(diǎn)原因,一是虛擬機(jī)規(guī)范規(guī)定,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行()方法完畢,二是類的靜態(tài)屬性只會在第一次加載類的時候初始化。

          綜合這兩點(diǎn)以及第5行代碼,在多線程環(huán)境,只會有一個線程創(chuàng)建一個實(shí)例,其他線程用的都是這個線程創(chuàng)建的這個實(shí)例。

          (6)、枚舉 (可用、推薦)

          public enum Singleton {
              INSTANCE;
          }

          這種方式是《Effective JAVA》作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機(jī)制,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實(shí)例化。

          單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法
                                -- 出自 《effective java》

          (7)、在實(shí)際開發(fā)中,選擇哪種單例模式的實(shí)現(xiàn)方式?

          一般情況下,不建議使用第2種和第3種懶漢方式,建議使用第1種餓漢方式。只有在要明確實(shí)現(xiàn) lazy loading 效果時,才會使用第5種靜態(tài)內(nèi)部類方式。如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用第6種枚舉方式。如果有其他特殊的需求,可以考慮使用第4種雙重校驗(yàn)鎖方式。

          三、單例模式優(yōu)點(diǎn)

          (1)在內(nèi)存里只有一個實(shí)例,減少了內(nèi)存開銷,特別是一個對象需要頻繁創(chuàng)建和銷毀,而且創(chuàng)建和銷毀時的性能又無法優(yōu)化時,這個時候,把它設(shè)置成單例可以提高系統(tǒng)性能。
          (2)可以避免對資源的多重占用,比如對一個文件進(jìn)行寫操作,由于只有一個連接實(shí)例存在內(nèi)存中,可以避免對一個資源文件同時進(jìn)行寫操作。
          (3)設(shè)置全局訪問點(diǎn),嚴(yán)格控制訪問。例如Web應(yīng)用的頁面計(jì)數(shù)器就可以用單例模式來實(shí)現(xiàn),從而保證計(jì)數(shù)的準(zhǔn)確性。

          四、單例模式缺點(diǎn)

          (1)不適用于變化頻繁的對象,如果同一類型的對象總是要在不同的用例場景發(fā)生變化,單例模式就會引起數(shù)據(jù)的錯誤,不能保存彼此的狀態(tài)。
          (2)由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。
          (3)單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。
          (3)如果實(shí)例化的對象長時間不被利用,系統(tǒng)會認(rèn)為該對象是垃圾而被回收,可能會導(dǎo)致對象狀態(tài)的丟失。

          五、單例模式的使用場景

          (1)要求一個類在程序的生命周期當(dāng)中只有一個實(shí)例。比如在Spring框架項(xiàng)目中,若某個類只需要有一個實(shí)例,那么只要把Spring配置文件該類的的scope屬性配置成singleton即可。
          (2)需要頻繁實(shí)例化然后銷毀的對象,也就是頻繁的 new 對象,可以考慮用單例模式替換。
          (3)創(chuàng)建對象時耗時過多或者耗資源過多,但又經(jīng)常用到的對象,比如數(shù)據(jù)庫連接。

          下面我們舉個例子,講解單例模式的使用:網(wǎng)站在線人數(shù)統(tǒng)計(jì)

          其實(shí)就是一個全局計(jì)數(shù)器,也就是說所有用戶在相同的時刻獲取到的在線人數(shù)數(shù)量都是一致的。要實(shí)現(xiàn)這個需求,計(jì)數(shù)器就要全局唯一,也就正好可以用單例模式來實(shí)現(xiàn)(利用單例模式的第三個優(yōu)點(diǎn):全局訪問)。

          public class Counter {
              // 使用靜態(tài)類實(shí)現(xiàn)單例模式
              private static class CounterHolder{
                  private static final Counter counter = new Counter();
              }
              // 私有構(gòu)造器
              private Counter(){
                  System.out.println("init...");
              }
              // 獲取實(shí)例的方法
              public static final Counter getInstance(){
                  return CounterHolder.counter;
              }
              // 使用AtomicLong類型存儲計(jì)數(shù)
              private AtomicLong online = new AtomicLong();
              // 獲取計(jì)數(shù)
              public long getOnline(){
                  return online.get();
              }
              // 計(jì)數(shù)加1
              public long add(){
                  return online.incrementAndGet();
              }
          }  


          瀏覽 30
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  蜜桃成人无码AV在线观看一电影 | 夜夜拍拍| 九九九免费 | 高清无码小视频 | 日韩黄色成人视频 |