天天看點

深入設計模式08---單例模式

前言

今天看了一部電影《末代皇帝》,講述的是溥儀的一生,也是一個時代的末尾,看的心中很有感觸,人生隻有一次,每個人都無法選擇兩個人生,是以我們都要努力的去過上精彩的人生,不給以後留遺憾,順應時代,努力進步。

今天要學習的是單例模式,單例模式是一種最簡單的設計模式,我相信大家應該對這種模式是最了解的,它的核心是一個被稱作單例類的特殊類,通過單例模式可以確定系統中的單例類隻有一個執行個體,而且該執行個體可以被外部通路以此來節省系統資源。

雖然單例模式的結構很簡單,但是他其中的一些細節卻非常值得注意,希望大家可以通過本篇部落格對簡單的單例模式有一個更加全面的認識。

正文

  • 概述

    確定一個類隻有一個執行個體,并且提供一個全局通路點來通路這個唯一的執行個體。

    單例模式的設計思想如下:

    如何確定一個類隻有一個執行個體呢?單例設計模式有三個要點:一是某個類隻能有一個執行個體,二是它必須自行建立這執行個體,三是它必須自行向整個系統提供這個執行個體。

  • 結構與實作

    • 模式結構
      • 單例模式的結構很簡單,它隻包含一個單例類,在該單例類内部建立一個它自身的執行個體,為了防止外部對單例類執行個體化,可以私有化它的構造函數,在它的内部定義一個private的單例對象,然後提供一個公開的方法向外界提供該對象。

        案例說明:

        某公司承接了一個負載均衡(Load Balance)器軟體的開發工作,該軟體運作在一台負載均衡伺服器上,可以将并發通路和資料流量分發到伺服器叢集中的多台裝置上進行并發處理,提高系統的整體處理能力,縮短響應時間。由于叢集中的伺服器需要動态的删減,且用戶端請求需要統一分發,是以需要確定負載均衡器的唯一性,試用單例模式設計該負載均衡器。

        案例分析:

        單例的負載均衡器中需要包含一個容器,用來存儲伺服器,客戶可以動态的添加或者删除伺服器,并且可以擷取伺服器(分發)。

        目錄結構:

        深入設計模式08---單例模式
        LoadBalance: 負載均衡器(模拟負載)。
        public class LoadBalance {
        
            private static LoadBalance loadBalance = null;
        
            private List<String> serverList = null;
        
            private LoadBalance(){
                serverList = new ArrayList();
            }
        
            public static LoadBalance getLoadBalance(){
                if (null == loadBalance){
                    loadBalance = new LoadBalance();
                }
                return loadBalance;
            }
        
            public void addServer(String server){
                serverList.add(server);
            }
        
            public void removeServer(String server){
                serverList.remove(server);
            }
        
            // 随機擷取伺服器
            public String getServer(){
                Random random = new Random();
                int idx = random.nextInt(serverList.size());
                return serverList.get(idx);
            }
        
        }
                   
        Client(用戶端測試類): 模拟請求,負載進行分發(隻貼出了main方法)。
        /**
         * 需求:
         *      某公司承接了一個負載均衡(Load Balance)器軟體的開發工作,該軟體運作在一台負載均衡伺服器上,可以将并發通路和資料流量分發到伺服器叢集中的
         *      多台裝置上進行并發處理,提高系統的整體處理能力,縮短響應時間。由于叢集中的伺服器需要動态的删減,且用戶端請求需要統一分發,是以需要確定
         *      負載均衡器的唯一性,試用單例模式設計該負載均衡器。
         * 分析:
         *      單例的負載均衡器中需要包含一個容器,用來存儲伺服器,客戶可以動态的添加或者删除伺服器,并且可以擷取伺服器(分發);
         * @param args
         */
        public static void main(String[] args) {
        	LoadBalance loadBalance1 = LoadBalance.getLoadBalance();
            LoadBalance loadBalance2 = LoadBalance.getLoadBalance();
            LoadBalance loadBalance3 = LoadBalance.getLoadBalance();
            if (loadBalance1 == loadBalance2 && loadBalance2== loadBalance3 && loadBalance1 == loadBalance3){
                System.out.println("負載均衡器是唯一的!!");
            }
        
            loadBalance1.addServer("Server 1");
            loadBalance2.addServer("Server 2");
            loadBalance3.addServer("Server 3");
        
            LoadBalance loadBalance4 = LoadBalance.getLoadBalance();
            for (int i = 0; i < 10; i++) {
                System.out.println("請求分發到伺服器: "+loadBalance4.getServer());
            }
        }
                   
        運作結果:
        深入設計模式08---單例模式
        可以看出,負載均衡器在該系統中是單例的,不管擷取幾次擷取到的始終都是同一個單例。
  • 深入單例的建立

    餓漢式單例: 餓漢式單例實作起來非常簡單,在單例類中聲明一個靜态的變量,直接将單例對象執行個體化出來,然後提供方法向外界提供方法可以直接調用。

    餓漢式單例代碼:

    public class EagerSingleton {
    
        private static final EagerSingleton instance = new EagerSingleton();
    
        private EagerSingleton(){};
    
        public static EagerSingleton getInstance(){
            return instance;
        }
    
    }
               

    在單例類被加載時,餓漢式單例會直接随着類的加載直接将單例類建立出來,但是這樣會更加的小号系統資源,因為我們無論是否使用單例,在系統加載該類時都會建立單例對象,一定程度上會延長系統的加載速度。

    懶漢式單例: 懶漢式單例是使用比較頻繁的一種方式,在加載類時我們不直接建立單例對象類,在真正要使用時我們才來建立單例對象,大家看下代碼就明白了。

    懶漢式單例代碼:

    public class LazySubgletion {
    
        // 使用volatile關鍵字保證多線程中都能正确處理(可以了解為被volatile修飾的屬性直接屬于主線程,通常說main的可見性)
        private static  LazySubgletion instance = null;
    
        private LazySubgletion(){};
    
        public static LazySubgletion getInstance(){  
             if (instance == null){
                 instance = new LazySubgletion();
             }
            return instance;
        }
    }
               

    在第一次使用單例對象時才建立,這種方式就是懶漢式(也被稱為懶加載),

    但是這種方式需要注意線程安全,我們要避免多個線程同時調用getInstance方法,可以通過以下方式避免:

    public class LazySubgletion {
    
        // 使用volatile關鍵字保證多線程中都能正确處理(可以了解為被volatile修飾的屬性直接屬于主線程,通常說main的可見性)
        private volatile static  LazySubgletion instance = null;
    
        private LazySubgletion(){};
    
        /**
         * 注意需要保證線程安全,采用雙重檢查鎖定的方式保證線程安全
         * @return
         */
        public static LazySubgletion getInstance(){
            // 第一重判斷,當為null時進入
            if (instance == null){
                // 如果有一個線程進入,鎖定,但是此時還沒有建立instance,其它線程進入後會進行排隊(有可能會建立多個)
                // 是以就需要有第二重判斷,在進入synchronized代碼塊後再次判斷是否為null,為null再建立
                synchronized (LazySubgletion.class){
                    if (instance == null){
                        instance = new LazySubgletion();
                    }
                }
            }
            return instance;
        }
    
    }
               
    這種方式叫做雙重檢查鎖,為什麼要使用雙重檢查鎖來鎖定呢?我們可以考慮一下,當A和B線程同時調用的getInstance()方法時,這是倆線程一看instance都是null,然後倆個線程都想進入,如果A線程先進來synchronized代碼塊,代碼塊被鎖定,B線程進入了排隊的狀态,然後A線程建立出了單例對象,但是這時的B已經在等待synchronized代碼塊了,A執行完後B緊跟着進入了代碼塊,因為B不知道instance已經被建立了,所有他也建立了一次對象,這樣就導緻系統線程不安全了,是以我們需要雙重檢查鎖(即在synchronized代碼塊中再進行一次判斷),同時我們還要給單例對象instance加上volatile修飾符,加了該修飾符表示被修飾的變量對main線程直接可見,就是保證大家看到的該變量都是一緻的,這樣可以較為完美的解決線程安全的問題。
    補充

    靜态内部類實作單例: 餓漢式不管單例對象用不用都會占用記憶體,而懶漢式雖然可以實作延遲加載,但是我們需要控制線程安全,過程較為繁瑣,同樣也會影響系統性能,那麼有沒有更好的方式來實作單例模式呢?

    當然!有一種靜态内部類的方式可以更加完美的實作單例模式!

    我們先看一下代碼:

// 使用靜态内部類的方式實作(堪稱完美的實作方案)
public class StaticSingletion {

    private StaticSingletion() {
    }

    private static class HolderClass{
        private final static StaticSingletion instance = new StaticSingletion();
    }

    /**
     * 在第一次加載時将調用内部類HolderClass且該類為靜态内部類,隻會被加載一次,在該類内部定義了一個static類型的StaticSingletion,
     * 由Java虛拟機保證其線程的安全性,由于getInstance不需要再被任何線程鎖定,是以對性能不會造成影響。
     * @return
     */
    public static StaticSingletion getInstance(){
        return HolderClass.instance;
    }

    public static void main(String[] args) {
        StaticSingletion s1,s2;
        s1 = StaticSingletion.getInstance();
        s2 = StaticSingletion.getInstance();
        System.out.println(s1 == s2);
    }
}
           

在這種方式實作的單例下,我們将單例對象聲明在了靜态内部類中,單例對象會随着靜态内部類的加載而被建立出來,我們在getInstance方法中調用靜态内部類的instance執行個體,由于執行個體是被static修飾的,它直接屬于内部類,會随着内部類的加載而加載,保證了單例的唯一性,然後我們在調用内部類加載時會被虛拟機加載,虛拟機控制了線程的安全性,這樣對性能的影響是最小的,這種方式既可以實作延遲加載,又可以保證線程安全且不影響系統性能,它的缺點是這是Java語言的特性,有許多的其它面向對象語言并不支援該方式。

  • 優缺點分析

    • 優點
      • 單例模式提供了唯一執行個體的受控通路,因為單例類封裝了它的唯一執行個體,索引它可以嚴格的控制使用者怎樣以及何時可以通路它;
      • 在系統記憶體中始終隻存在一個對象,節省系統資源;
      • 允許可變數目的執行個體,可以基于單例類進行擴充,使用與控制單例相似的方式擷取指定個數的執行個體。
    • 缺點
      • 單例模式沒有抽象層,是以很難擴充;
      • 單例類的職責過重,違背了單一職責原則;
      • 由于虛拟機的垃圾自動回收技術,如果單例對象長時間不被利用,系統會把它當作辣雞回收掉,會導緻單例的已有的一些狀态丢失。
    • 适用環境

      在以下情況下可以考慮使用原型模式:

      1. 系統隻需要一個執行個體對象;

      2. 用戶端調用類的單個執行個體隻允許使用同一個公共通路點,不能通過其它方式進行通路。

  • 自練習習題

    • 某軟體開發人員要建立一個資料庫連接配接池,将指定個數的資料庫連接配接對象存儲在連接配接池中,用戶端代碼可以從池中随機擷取一個連接配接對象來連接配接資料庫 ,試通過單例類進行改造,設計一個能夠自行提供指定個數執行個體對象的資料庫連接配接類并用Java代碼模拟程式設計。

    世界上再無第二個我,人生來孤獨,努力讓自己變的優秀起來!

    半原創部落格,用以記錄學習,希望可以幫到您,不喜可噴。
    深入設計模式08---單例模式

繼續閱讀