從例項出發,瞭解單例模式和靜態塊

風的姿態發表於2018-05-29

就算你沒有用到過其他的設計模式,但是單例模式你肯定接觸過,比如,Spring 中 bean 預設就是單例模式的,所有用到這個 bean 的例項其實都是同一個。

單例模式的使用場景

什麼是單例模式呢,單例模式(Singleton)又叫單態模式,它出現目的是為了保證一個類在系統中只有一個例項,並提供一個訪問它的全域性訪問點。從這點可以看出,單例模式的出現是為了可以保證系統中一個類只有一個例項而且該例項又易於外界訪問,從而方便對例項個數的控制並節約系統資源而出現的解決方案。

使用單例模式當然是有原因,有好處的了。在下面幾個場景中適合使用單例模式:

1、有頻繁例項化然後銷燬的情況,也就是頻繁的 new 物件,可以考慮單例模式;

2、建立物件時耗時過多或者耗資源過多,但又經常用到的物件;

3、頻繁訪問 IO 資源的物件,例如資料庫連線池或訪問本地檔案;

下面舉幾個例子來說明一下:

1、網站線上人數統計;

其實就是全域性計數器,也就是說所有使用者在相同的時刻獲取到的線上人數數量都是一致的。要實現這個需求,計數器就要全域性唯一,也就正好可以用單例模式來實現。當然這裡不包括分散式場景,因為計數是存在記憶體中的,並且還要保證執行緒安全。下面程式碼是一個簡單的計數器實現。

public class Counter {
   
    private static class CounterHolder{
        private static final Counter counter = new Counter();
    }

    private Counter(){
        System.out.println("init...");
    }

    public static final Counter getInstance(){
        return CounterHolder.counter;
    }

    private AtomicLong online = new AtomicLong();

    public long getOnline(){
        return online.get();
    }

    public long add(){
        return online.incrementAndGet();
    }
}    

2、配置檔案訪問類;

專案中經常需要一些環境相關的配置檔案,比如簡訊通知相關的、郵件相關的。比如 properties 檔案,這裡就以讀取一個properties 檔案配置為例,如果你使用的 Spring ,可以用 @PropertySource 註解實現,預設就是單例模式。如果不用單例的話,每次都要 new 物件,每次都要重新讀一遍配置檔案,很影響效能,如果用單例模式,則只需要讀取一遍就好了。以下是檔案訪問單例類簡單實現:

public class SingleProperty {

    private static Properties prop;

    private static class SinglePropertyHolder{
        private static final SingleProperty singleProperty = new SingleProperty();
    }

    /**
    * config.properties 內容是 test.name=kite 
    */
    private SingleProperty(){
        System.out.println("建構函式執行");
        prop = new Properties();
        InputStream stream = SingleProperty.class.getClassLoader()
                .getResourceAsStream("config.properties");
        try {
            prop.load(new InputStreamReader(stream, "utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SingleProperty getInstance(){
        return SinglePropertyHolder.singleProperty;
    }
    
    
    public String getName(){
        return prop.get("test.name").toString();
    }

    public static void main(String[] args){
        SingleProperty singleProperty = SingleProperty.getInstance();
        System.out.println(singleProperty.getName());
    }
}

3、資料庫連線池的實現,也包括執行緒池。為什麼要做池化,是因為新建連線很耗時,如果每次新任務來了,都新建連線,那對效能的影響實在太大。所以一般的做法是在一個應用內維護一個連線池,這樣當任務進來時,如果有空閒連線,可以直接拿來用,省去了初始化的開銷。所以用單例模式,正好可以實現一個應用內只有一個執行緒池的存在,所有需要連線的任務,都要從這個連線池來獲取連線。如果不使用單例,那麼應用內就會出現多個連線池,那也就沒什麼意義了。如果你使用 Spring 的話,並整合了例如 druid 或者 c3p0 ,這些成熟開源的資料庫連線池,一般也都是預設以單例模式實現的。

單例模式的實現方法

如果你在書上或者網站上搜尋單例模式的實現,一般都會介紹5、6中方式,其中有一些隨著 Java 版本的升高,以及多執行緒技術的使用變得不那麼實用了,這裡就介紹兩種即高效,而且又是執行緒安全的方式。

1. 靜態內部類方式

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

這種寫法仍然使用 JVM 本身機制保證了執行緒安全問題,由於 SingletonHolder 是私有的,除了 getInstance() 方法外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴 JDK 版本。上面的兩個例子就是用這種方式實現的。

2. 列舉方式

public enum SingleEnum {

    INSTANCE;

    SingleEnum(){
        System.out.println("建構函式執行");
    }

    public String getName(){
        return "singleEnum";
    }

    public static void main(String[] args){
        SingleEnum singleEnum = SingleEnum.INSTANCE;
        System.out.println(singleEnum.getName());
    }
}

我們可以通過 SingleEnum.INSTANCE 來訪問例項。而且建立列舉預設就是執行緒安全的,並且還能防止反序列化導致重新建立新的物件。

靜態塊

什麼是靜態塊呢。

1、它是隨著類的載入而執行,只執行一次,並優先於主函式。具體說,靜態程式碼塊是由類呼叫的。類呼叫時,先執行靜態程式碼塊,然後才執行主函式的;

2、靜態程式碼塊其實就是給類初始化的,而構造程式碼塊是給物件初始化的;

3、靜態程式碼塊中的變數是區域性變數,與普通函式中的區域性變數性質沒有區別;

4、一個類中可以有多個靜態程式碼塊;

他的寫法是這樣的:

static {
        System.out.println("static executed");
    }

來看一下下面這個完整的例項:

public class SingleStatic {

    static {
        System.out.println("static 塊執行中...");
    }

    {
        System.out.println("構造程式碼塊 執行中...");
    }

    public SingleStatic(){
        System.out.println("建構函式 執行中");
    }

    public static void main(String[] args){
        System.out.println("main 函式執行中");
        SingleStatic singleStatic = new SingleStatic();
    }

}

他的執行結果是這樣的:

static 塊執行中...
main 函式執行中
構造程式碼塊 執行中...
建構函式 執行中

從中可以看出他們的執行順序分別為:

1、靜態程式碼塊

2、main 函式

3、構造程式碼塊

4、建構函式

利用靜態程式碼塊只在類載入的時候執行,並且只執行一次這個特性,也可以用來實現單例模式,但是不是懶載入,也就是說每次類載入就會主動觸發例項化。

除此之外,不考慮單例的情況,利用靜態程式碼塊的這個特性,可以實現其他的一些功能,例如上面提到的配置檔案載入的功能,可以在類載入的時候就讀取配置檔案的內容,相當於一個預載入的功能,在使用的時候可以直接拿來就用。

不妨拿出手機掃一下,關注我的微信公眾號,關注後可點選“加群”加入微信交流群

從例項出發,瞭解單例模式和靜態塊

相關文章