Java單例模式的深入瞭解

一、設計模式概覽

1.1、軟件設計模式的概念

軟件設計模式(Software Design Pattern),又稱設計模式,是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。它描述瞭在軟件設計過程中的一些不斷重復發生的問題,以及該問題的解決方案。也就是說,它是解決特定問題的一系列套路,是前輩們的代碼設計經驗的總結,具有一定的普遍性,可以反復使用。其目的是為瞭提高代碼的可重用性、代碼的可讀性和代碼的可靠性。

1.2、軟件設計模式的基本要素

1. 模式名稱

 每一個模式都有自己的名字,通常用一兩個詞來描述,可以根據模式的問題、特點、解決方案、功能和效果來命名。模式名稱(PatternName)有助於我們理解和記憶該模式,也方便我們來討論自己的設計。

2. 問題

問題(Problem)描述瞭該模式的應用環境,即何時使用該模式。它解釋瞭設計問題和問題存在的前因後果,以及必須滿足的一系列先決條件。

3. 解決方案

模式問題的解決方案(Solution)包括設計的組成成分、它們之間的相互關系及各自的職責和協作方式。因為模式就像一個模板,可應用於多種不同場合,所以解決方案並不描述一個特定而具體的設計或實現,而是提供設計問題的抽象描述和怎樣用一個具有一般意義的元素組合(類或對象的 組合)來解決這個問題。

4. 效果

描述瞭模式的應用效果以及使用該模式應該權衡的問題,即模式的優缺點。主要是對時間和空間的衡量,以及該模式對系統的靈活性、擴充性、可移植性的影響,也考慮其實現問題。顯式地列出這些效果(Consequence)對理解和評價這些模式有很大的幫助。

1.3、GoF 的 23 種設計模式的分類和功能

1. 根據目的來分

根據模式是用來完成什麼工作來劃分,

創建型模式:用於描述“怎樣創建對象”,它的主要特點是“將對象的創建與使用分離”。GoF 中提供瞭單例、原型、工廠方法、抽象工廠、建造者等 5 種創建型模式。

結構型模式:用於描述如何將類或對象按某種佈局組成更大的結構,GoF 中提供瞭代理、適配器、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。

行為型模式:用於描述類或對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務,以及怎樣分配職責。GoF 中提供瞭模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、解釋器等 11 種行為型模式。

2. GoF的23種設計模式的功能

單例(Singleton)模式:某個類隻能生成一個實例,該類提供瞭一個全局訪問點供外部獲取該實例,其拓展是有限多例模式。

原型(Prototype)模式:將一個對象作為原型,通過對其進行復制而克隆出多個和原型類似的新實例。

工廠方法(Factory Method)模式:定義一個用於創建產品的接口,由子類決定生產什麼產品。

抽象工廠(AbstractFactory)模式:提供一個創建產品族的接口,其每個子類可以生產一系列相關的產品。

建造者(Builder)模式:將一個復雜對象分解成多個相對簡單的部分,然後根據不同需要分別創建它們,最後構建成該復雜對象。

代理(Proxy)模式:為某對象提供一種代理以控制對該對象的訪問。即客戶端通過代理間接地訪問該對象,從而限制、增強或修改該對象的一些特性。

適配器(Adapter)模式:將一個類的接口轉換成客戶希望的另外一個接口,使得原本由於接口不兼容而不能一起工作的那些類能一起工作。

橋接(Bridge)模式:將抽象與實現分離,使它們可以獨立變化。它是用組合關系代替繼承關系來實現,從而降低瞭抽象和實現這兩個可變維度的耦合度。

裝飾(Decorator)模式:動態的給對象增加一些職責,即增加其額外的功能。

外觀(Facade)模式:為多個復雜的子系統提供一個一致的接口,使這些子系統更加容易被訪問。

享元(Flyweight)模式:運用共享技術來有效地支持大量細粒度對象的復用。

組合(Composite)模式:將對象組合成樹狀層次結構,使用戶對單個對象和組合對象具有一致的訪問性。

模板方法(TemplateMethod)模式:定義一個操作中的算法骨架,而將算法的一些步驟延遲到子類中,使得子類可以不改變該算法結構的情況下重定義該算法的某些特定步驟。

策略(Strategy)模式:定義瞭一系列算法,並將每個算法封裝起來,使它們可以相互替換,且算法的改變不會影響使用算法的客戶。

命令(Command)模式:將一個請求封裝為一個對象,使發出請求的責任和執行請求的責任分割開。

職責鏈(Chain of Responsibility)模式:把請求從鏈中的一個對象傳到下一個對象,直到請求被響應為止。通過這種方式去除對象之間的耦合。

狀態(State)模式:允許一個對象在其內部狀態發生改變時改變其行為能力。

觀察者(Observer)模式:多個對象間存在一對多關系,當一個對象發生改變時,把這種改變通知給其他多個對象,從而影響其他對象的行為。

中介者(Mediator)模式:定義一個中介對象來簡化原有對象之間的交互關系,降低系統中對象間的耦合度,使原有對象之間不必相互瞭解。

迭代器(Iterator)模式:提供一種方法來順序訪問聚合對象中的一系列數據,而不暴露聚合對象的內部表示。

訪問者(Visitor)模式:在不改變集合元素的前提下,為一個集合中的每個元素提供多種訪問方式,即每個元素有多個訪問者對象訪問。

備忘錄(Memento)模式:在不破壞封裝性的前提下,獲取並保存一個對象的內部狀態,以便以後恢復它。

解釋器(Interpreter)模式:提供如何定義語言的文法,以及對語言句子的解釋方法,即解釋器。

1.4、軟件設計的七大原則

軟件設計的七大原則
原則 描述 作用
開閉原則 對擴展開放,對修改關閉 降低維護帶來的新風險
依賴倒置原則 高層不應該依賴低層,要面向接口編程 更利於代碼結構的升級擴展
單一職責原則 一個類隻幹一件事,實現類要單一 便於理解,提高代碼的可讀性
接口隔離原則 一個接口隻幹一件事,接口要精簡單一 功能解耦,高聚合、低耦合
迪米特法則 不該知道的不要知道,一個類應該保持對其它對象最少的瞭解,降低耦合度 隻和朋友交流,不和陌生人說話,減少代碼臃腫
裡氏替換原則 不要破壞繼承體系,子類重寫方法功能發生改變,不應該影響父類方法的含義 防止繼承泛濫
合成復用原則 盡量使用組合或者聚合關系實現代碼復用,少使用繼承 降低代碼耦合

實際上,這些原則的目的隻有一個:降低對象之間的耦合,增加程序的可復用性、可擴展性和可維護性。

記憶口訣:訪問加限制,函數要節儉,依賴不允許,動態加接口,父類要抽象,擴展不更改。

在程序設計時,我們應該將程序功能最小化,每個類隻幹一件事。若有類似功能基礎之上添加新功能,則要合理使用繼承。對於多方法的調用,要會運用接口,同時合理設置接口功能與數量。最後類與類之間做到低耦合高內聚。

 二、單利模式

1.1、單例模式的相關定義

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

單利模式三個特點

單例類隻有一個實例對象;

該單例對象必須由單例類自行創建;

單例類對外提供一個訪問該單例的全局訪問點。  

單例模式的優點

單例模式可以保證內存裡隻有一個實例,減少瞭內存的開銷。

可以避免對資源的多重占用。

單例模式設置全局訪問點,可以優化和共享資源的訪問。

單例模式的缺點:

單例模式一般沒有接口,擴展困難。如果要擴展,則除瞭修改原來的代碼,沒有第二種途徑,違背開閉原則。

在並發測試中,單例模式不利於代碼調試。在調試過程中,如果單例中的代碼沒有執行完,也不能模擬生成一個新的對象。

單例模式的功能代碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則。

單例模式的應用場景

需要頻繁創建的一些類,使用單例可以降低系統的內存壓力,減少 GC。

某類隻要求生成一個對象的時候,如一個班中的班長、每個人的身份證號等。

某些類創建實例時占用資源較多,或實例化耗時較長,且經常使用。

某類需要頻繁實例化,而創建的對象又頻繁被銷毀的時候,如多線程的線程池、網絡連接池等。

頻繁訪問數據庫或文件的對象。

對於一些控制硬件級別的操作,或者從系統上來講應當是單一控制邏輯的操作,如果有多個實例,則系統會完全亂套。

當對象需要被共享的場合。由於單例模式隻允許創建一個對象,共享該對象可以節省內存,並加快對象訪問速度。如 Web 中的配置對象、數據庫的連接池等。

1.2、單利模式的結構

單例類:包含一個實例且能自行創建這個實例的類。

訪問類:使用單例的類。

 

 

2.1單利模式的實現方式一:懶漢式

該模式的特點是類加載時沒有生成單例,隻有當第一次調用 getlnstance 方法時才去創建這個單例。

代碼:

public class LazySingleton {
 
        //構造器私有,堵死瞭外界利用new創建此類對象的可能
        private LazySingleton(){
                System.out.println("yese");
 
        }
        //提供對象,是外界獲取本類對象的唯一全局訪問點
        private static LazySingleton lazySingleton;
 
        public static  LazySingleton getInstance(){
                //如果對象不存在,就new一個新的對象,否則返回已有的對象
                if(lazySingleton == null) {
                        lazySingleton = new LazySingleton();
                }
                return lazySingleton;
        }
}
 @Test
    public void testLazy() {
        LazySingleton l1 = LazySingleton.getInstance();
        LazySingleton l2 = LazySingleton.getInstance();
        System.out.println(l1+"\n" + l2);
        System.out.println(l1 == l2);
    }

測試結果:  

[email protected]
[email protected]
true

 該代碼很顯然是不適合多線程模式的,這時候就需要給單例模式的對象加上一把鎖

class LazySingleton2{
        //構造器私有,堵死瞭外界利用new創建此類對象的可能
        private LazySingleton2(){
                System.out.println("s2");
 
        }
        //提供對象,是外界獲取本類對象的唯一全局訪問點
        private static LazySingleton2 lazySingleton2;
 
        public static  LazySingleton2 getInstance(){
                //如果對象不存在,就new一個新的對象,否則返回已有的對象
                if(lazySingleton2 == null) {
                        synchronized(LazySingleton2.class) {
                                if(lazySingleton2 == null) {
                                        lazySingleton2 = new LazySingleton2();
                                }
                        }
                }
                return lazySingleton2;
        }
}

這時還有一個問題,就是在創建一個對象時,在JVM中會經過三步:

(1)為對象分配內存空間

(2)初始化該對象

(3)將該對象指向分配好的內存空間

 那麼在執行我們這個多線程代碼的過程中,有可能他不按照這三部的順序走,那麼就會導致一個指令重排的問題,解決此問題的方法就是加關鍵字volatile

class LazySingleton3{
        //構造器私有,堵死瞭外界利用new創建此類對象的可能
        private LazySingleton3(){
        }
        //提供對象,是外界獲取本類對象的唯一全局訪問點
        private volatile static LazySingleton3 lazySingleton3;
 
        public static  LazySingleton3 getInstance(){
                //如果對象不存在,就new一個新的對象,否則返回已有的對象
                if(lazySingleton3 == null) {
                        synchronized(LazySingleton3.class) {
                                if(lazySingleton3 == null) {
                                        lazySingleton3 = new LazySingleton3();
                                }
                        }
                }
                return lazySingleton3;
        }
}

最後我們還可以去解決一下反射來破壞單利模式

class LazySingleton4 {
    //提供變量,控制反射
    public static boolean s = false;
 
    //構造器私有,堵死瞭外界利用new創建此類對象的可能
    private LazySingleton4() {
        if (s == false) {
            //當第一個對象成功獲取之後,第二個對象就不能通過反射獲取瞭
            s = true;
        } else {
            throw new RuntimeException("不要用反射破壞異常");
        }
 
    }
 
    //提供對象,是外界獲取本類對象的唯一全局訪問點
    private volatile static LazySingleton4 lazySingleton4;
 
    public static LazySingleton4 getInstance() {
        //如果對象不存在,就new一個新的對象,否則返回已有的對象
        if (lazySingleton4 == null) {
            synchronized (LazySingleton4.class) {
                if (lazySingleton4 == null) {
                    lazySingleton4 = new LazySingleton4();
                }
            }
        }
        return lazySingleton4;
    }
}

volatile, synchronized這兩個關鍵字就能保證線程安全,但是每次訪問時都要同步,會影響性能,且消耗更多的資源,這是懶漢式單例的缺點。

2.2單利模式的實現方式一:餓漢式

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

代碼:

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

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

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: