模式設計之創建對象:單例模式(Singleton)詳解

在有些系統中,爲了節省內存資源、保證數據內容的一致性,對某些類要求只能創建一個實例,這就是所謂的單例模式。

單例模式的定義與特點

單例(Singleton)模式的定義:指一個類只有一個實例,且該類能自行創建這個實例的一種模式。例如,Windows 中只能打開一個任務管理器,這樣可以避免因打開多個任務管理器窗口而造成內存資源的浪費,或出現各個窗口顯示內容的不一致等錯誤。

在計算機系統中,還有 Windows 的回收站、操作系統中的文件系統、多線程中的線程池、顯卡的驅動程序對象、打印機的後臺處理服務、應用程序的日誌對象、數據庫的連接池、網站的計數器、Web 應用的配置對象、應用程序中的對話框、系統中的緩存等常常被設計成單例。

單例模式在現實生活中的應用也非常廣泛,例如公司 CEO、部門經理等都屬於單例模型。J2EE 標準中的 ServletContext 和 ServletContextConfig、Spring 框架應用中的 ApplicationContext、數據庫中的連接池等也都是單例模式。

單例模式有 3 個特點:

  1. 單例類只有一個實例對象;
  2. 該單例對象必須由單例類自行創建;
  3. 單例類對外提供一個訪問該單例的全局訪問點。

單例模式的優點和缺點

單例模式的優點:

  • 單例模式可以保證內存裏只有一個實例,減少了內存的開銷。
  • 可以避免對資源的多重佔用。
  • 單例模式設置全局訪問點,可以優化和共享資源的訪問。


單例模式的缺點:

  • 單例模式一般沒有接口,擴展困難。如果要擴展,則除了修改原來的代碼,沒有第二種途徑,違背開閉原則。
  • 在併發測試中,單例模式不利於代碼調試。在調試過程中,如果單例中的代碼沒有執行完,也不能模擬生成一個新的對象。
  • 單例模式的功能代碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則。

單例模式看起來非常簡單,實現起來也非常簡單。單例模式在面試中是一個高頻面試題。希望大家能夠認真學習,掌握單例模式,提升核心競爭力,給面試加分,順利拿到 Offer。

單例模式的應用場景

對於 Java 來說,單例模式可以保證在一個 JVM 中只存在單一實例。單例模式的應用場景主要有以下幾個方面。

  • 需要頻繁創建的一些類,使用單例可以降低系統的內存壓力,減少 GC。
  • 某類只要求生成一個對象的時候,如一個班中的班長、每個人的身份證號等。
  • 某些類創建實例時佔用資源較多,或實例化耗時較長,且經常使用。
  • 某類需要頻繁實例化,而創建的對象又頻繁被銷燬的時候,如多線程的線程池、網絡連接池等。
  • 頻繁訪問數據庫或文件的對象。
  • 對於一些控制硬件級別的操作,或者從系統上來講應當是單一控制邏輯的操作,如果有多個實例,則系統會完全亂套。
  • 當對象需要被共享的場合。由於單例模式只允許創建一個對象,共享該對象可以節省內存,並加快對象訪問速度。如 Web 中的配置對象、數據庫的連接池等。

單例模式的結構與實現

單例模式是設計模式中最簡單的模式之一。通常,普通類的構造函數是公有的,外部類可以通過“new 構造函數()”來生成多個實例。但是,如果將類的構造函數設爲私有的,外部類就無法調用該構造函數,也就無法生成多個實例。這時該類自身必須定義一個靜態私有實例,並向外提供一個靜態的公有函數用於創建或獲取該靜態私有實例。

下面來分析其基本結構和實現方法。

1. 單例模式的結構

單例模式的主要角色如下。

  • 單例類:包含一個實例且能自行創建這個實例的類。
  • 訪問類:使用單例的類。


其結構如圖 1 所示。

圖1 單例模式的結構圖

2. 單例模式的實現

Singleton 模式通常有兩種實現形式。

第 1 種:懶漢式單例

該模式的特點是類加載時沒有生成單例,只有當第一次調用 getlnstance 方法時纔去創建這個單例。代碼如下:

public class LazySingleton {
    private static volatile LazySingleton instance = null;    //保證 instance 在所有線程中同步

    private LazySingleton() {
    }    //private 避免類在外部被實例化

    public static synchronized LazySingleton getInstance() {
        //getInstance 方法前加同步
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

注意:如果編寫的是多線程程序,則不要刪除上例代碼中的關鍵字 volatile 和 synchronized,否則將存在線程非安全的問題。如果不刪除這兩個關鍵字就能保證線程安全,但是每次訪問時都要同步,會影響性能,且消耗更多的資源,這是懶漢式單例的缺點。

第 2 種:餓漢式單例

該模式的特點是類一旦加載就創建一個單例,保證在調用 getInstance 方法之前單例已經存在了。

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

餓漢式單例在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不再改變,所以是線程安全的,可以直接用於多線程而不會出現問題。

單例模式的應用實例

【例1】用懶漢式單例模式模擬產生美國當今總統對象。

分析:在每一屆任期內,美國的總統只有一人,所以本實例適合用單例模式實現,圖 2 所示是用懶漢式單例實現的結構圖。

圖2 美國總統生成器的結構圖


程序代碼如下:

public class SingletonLazy {
    public static void main(String[] args) {
        President zt1 = President.getInstance();
        zt1.getName();    //輸出總統的名字
        President zt2 = President.getInstance();
        zt2.getName();    //輸出總統的名字
        if (zt1 == zt2) {
            System.out.println("他們是同一人!");
        } else {
            System.out.println("他們不是同一人!");
        }
    }
}

class President {
    private static volatile President instance = null;    //保證instance在所有線程中同步

    //private避免類在外部被實例化
    private President() {
        System.out.println("產生一個總統!");
    }

    public static synchronized President getInstance() {
        //在getInstance方法上加同步
        if (instance == null) {
            instance = new President();
        } else {
            System.out.println("已經有一個總統,不能產生新總統!");
        }
        return instance;
    }

    public void getName() {
        System.out.println("我是美國總統:特朗普。");
    }
}

運行結果:

產生一個總統!
我是美國總統:特朗普。
已經有一個總統,不能產生新總統!
我是美國總統:特朗普。
他們是同一人!

【例2】用餓漢式單例模式模擬產生豬八戒對象。

分析:同上例類似,豬八戒也只有一個,所以本實例同樣適合用單例模式實現。本實例由於要顯示豬八戒的圖像(點此下載該程序所要顯示的豬八戒圖片),所以用到了框架窗體 JFrame 組件,這裏的豬八戒類是單例類,可以將其定義成面板 JPanel 的子類,裏面包含了標籤,用於保存豬八戒的圖像,客戶窗體可以獲得豬八戒對象,並顯示它。圖 3 所示是用餓漢式單例實現的結構圖。

圖3 豬八戒生成器的結構圖


程序代碼如下:

import java.awt.*;
import javax.swing.*;

public class SingletonEager {
    public static void main(String[] args) {
        JFrame jf = new JFrame("餓漢單例模式測試");
        jf.setLayout(new GridLayout(1, 2));
        Container contentPane = jf.getContentPane();
        Bajie obj1 = Bajie.getInstance();
        contentPane.add(obj1);
        Bajie obj2 = Bajie.getInstance();
        contentPane.add(obj2);
        if (obj1 == obj2) {
            System.out.println("他們是同一人!");
        } else {
            System.out.println("他們不是同一人!");
        }
        jf.pack();
        jf.setVisible(true);
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

class Bajie extends JPanel {
    private static Bajie instance = new Bajie();

    private Bajie() {
        JLabel l1 = new JLabel(new ImageIcon("src/Bajie.jpg"));
        this.add(l1);
    }

    public static Bajie getInstance() {
        return instance;
    }
}

運行結果:

單例模式的擴展

單例模式可擴展爲有限的多例(Multitcm)模式,這種模式可生成有限個實例並保存在 ArrayList 中,客戶需要時可隨機獲取,其結構圖如圖 5 所示。

進階閱讀

如果您想了解單例模式在框架源碼中的應用,可猛擊閱讀《單例模式在JDK和Spring源碼中的應用》文章。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章