單例模式詳解

在Java開發過程當中,不少場景下都會碰到或要用到單例模式,在設計模式裏也是常常做爲指導學習的熱門模式之一,相信每位開發同事都用到過。咱們老是沿着前輩的足跡去作設定好的思路,每每沒去探究爲什麼這麼作,因此這篇文章對單例模式作了詳解。java

1、單例模式定義:編程

單例模式確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具備資源管理器的功能。每臺計算機能夠有若干個打印機,但只能有一個Printer Spooler,以免兩個打印做業同時輸出到打印機中。每臺計算機能夠有若干通訊端口,系統應當集中管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了不不一致狀態,避免政出多頭。設計模式

2、單例模式特色:
  一、單例類只能有一個實例。
  二、單例類必須本身建立本身的惟一實例。
  三、單例類必須給全部其餘對象提供這一實例。緩存

單例模式保證了全局對象的惟一性,好比系統啓動讀取配置文件就須要單例保證配置的一致性。安全

3、線程安全的問題多線程

一方面在獲取單例的時候,要保證不能產生多個實例對象,後面會詳細講到五種實現方式;併發

另外一方面,在使用單例對象的時候,要注意單例對象內的實例變量是會被多線程共享的,推薦使用無狀態的對象,不會由於多個線程的交替調度而破壞自身狀態致使線程安全問題,好比咱們經常使用的VO,DTO等(局部變量是在用戶棧中的,並且用戶棧自己就是線程私有的內存區域,因此不存在線程安全問題)。框架

4、單例模式的選擇性能

還記得咱們最先使用的MVC框架Struts1中的action就是單例模式的,而到了Struts2就使用了多例。在Struts1裏,當有多個請求訪問,每一個都會分配一個新線程,在這些線程,操做的都是同一個action對象,每一個用戶的數據都是不一樣的,而action卻只有一個。到了Struts2, action對象爲每個請求產生一個實例,並不會帶來線程安全問題(實際上servlet容器給每一個請求產生許多可丟棄的對象,可是並無影響到性能和垃圾回收問題,有時間會作下研究)。學習

5、實現單例模式的方式

1.餓漢式單例(當即加載方式)

// 餓漢式單例
public class Singleton1 {
    // 私有構造
    private Singleton1() {}

    private static Singleton1 single = new Singleton1();

    // 靜態工廠方法
    public static Singleton1 getInstance() {
        return single;
    }
}

餓漢式單例在類加載初始化時就建立好一個靜態的對象供外部使用,除非系統重啓,這個對象不會改變,因此自己就是線程安全的。

Singleton經過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的惟一實例只能經過getInstance()方法訪問。(事實上,經過Java反射機制是可以實例化構造方法爲private的類的,那基本上會使全部的Java單例實現失效。此問題在此處不作討論,姑且閉着眼就認爲反射機制不存在。)

2.懶漢式單例(延遲加載方式)

// 懶漢式單例
public class Singleton2 {

    // 私有構造
    private Singleton2() {}

    private static Singleton2 single = null;

    public static Singleton2 getInstance() {
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}

該示例雖然用延遲加載方式實現了懶漢式單例,但在多線程環境下會產生多個single對象,如何改造請看如下方式:

使用synchronized同步鎖

public class Singleton3 {
    // 私有構造
    private Singleton3() {}

    private static Singleton3 single = null;

    public static Singleton3 getInstance() {
        
        // 等同於 synchronized public static Singleton3 getInstance()
        synchronized(Singleton3.class){
          // 注意:裏面的判斷是必定要加的,不然出現線程安全問題
            if(single == null){
                single = new Singleton3();
            }
        }
        return single;
    }
}

在方法上加synchronized同步鎖或是用同步代碼塊對類加同步鎖,此種方式雖然解決了多個實例對象問題,可是該方式運行效率卻很低下,下一個線程想要獲取對象,就必須等待上一個線程釋放鎖以後,才能夠繼續運行。

public class Singleton4 {
    // 私有構造
    private Singleton4() {}
    
    //重點注意volatile
    private static volatile Singleton4 single = null;

    // 雙重檢查
    public static Singleton4 getInstance() {
        if (single == null) {   //操做①第一次檢查
            synchronized (Singleton4.class) {
                if (single == null) {   //操做②第二次檢查
                    single = new Singleton4(); //操做③
                }
            }
        }
        return single;
    }
}

使用雙重檢查進一步作了優化,能夠避免整個方法被鎖,只對須要鎖的代碼部分加鎖,能夠提升執行效率。

操做③能夠分解爲幾個獨立的子操做:

objRef = allocate(Singleton4.class);//子操做①:分配存儲空間
invokeConstructor(objRef);//子操做②:初始化objRef引用的對象
single = objRef;//子操做③:將對象引用寫入共享變量

根據鎖的重排序規則,臨界區內的操做能夠在臨界區內被重排。因此,JIN編譯器可能將上述子操做重排爲:①->③->②,即在初始化對象以前將對象的引用寫入實例變量single。因爲鎖對有序性的保障是有條件的,而操做①(第一次檢查)讀取single變量時沒有加鎖,所以上述重排序對操做①的執行線程是有影響的:該線程可能看到一個未初始化(或未初始化完成)的實例,即變量single的值不是null,可是該變量所引用的對象中的某些實例變量的變量值可能仍爲默認值,而不是構造器中設置的初始值。也就是說,一個線程在執行操做①的時候發現single不爲null,因而該線程就直接返回這個single變量鎖引用的實例,而這個實例多是未初始化完成的,這就可能致使錯誤。

解決:如上的重點注意,加上volatile關鍵字。

3.靜態內部類實現

public class Singleton6 {
    // 私有構造
    private Singleton6() {}

    // 靜態內部類
    private static class InnerObject{
        private static Singleton6 single = new Singleton6();
    }
    
    public static Singleton6 getInstance() {
        return InnerObject.single;
    }
}

靜態內部類雖然保證了單例在多線程併發下的線程安全性,可是在遇到序列化對象時,默認的方式運行獲得的結果就是多例的。這種狀況很少作說明了,使用時請注意。

4.static靜態代碼塊實現

public class Singleton6 {
    
    // 私有構造
    private Singleton6() {}
    
    private static Singleton6 single = null;

    // 靜態代碼塊
    static{
        single = new Singleton6();
    }
    
    public static Singleton6 getInstance() {
        return single;
    }
}

5.內部枚舉類實現

public class SingletonFactory {
    
    // 內部枚舉類
    private enum EnmuSingleton{
        Singleton;
        private Singleton8 singleton;
        
        //枚舉類的構造方法在類加載是被實例化 
        private EnmuSingleton(){
            singleton = new Singleton8();
        }
        public Singleton8 getInstance(){
            return singleton;
        }
    }
    public static Singleton8 getInstance() {
        return EnmuSingleton.Singleton.getInstance();
    }
}

class Singleton8{
    public Singleton8(){}
}

以上就是本文要介紹的全部單例模式的理解和實現,相信這篇文章能讓你們更清楚的理解單例模式,但願你們有問題能夠探討,多多指教!

備註:本文單例實現部分,實例源碼參照《Java多線程編程核心技術》-(高洪巖)一書中第六章的學習案例撰寫。 《java多線程編程實戰指南 核心篇》 - (黃文海)

相關文章
相關標籤/搜索