淺談singleton單例模式

1、前言java

單例模式比較簡單,能夠說沒有複雜的調用和接口的設計,就是一個簡單的類,只是要求這個類只生成一個對象,不管何時都要保證這一點,所以只能生成一個實例的模式就是單例模式。設計模式

2、類的加載緩存

類的加載是經過類加載器(Classloader)完成的,它既能夠是餓漢式加載類,也能夠是懶漢式加載,這跟不一樣的JVM實現有關。加載完類後,類的初始化就會發生,若是是對一個類的主動使用就會初始化對象,對類的被動使用不會對類進行初始化,好比final修飾的靜態變量若是能在編譯時就肯定變量的取值,會被當作常量,做爲對一個類的被動使用不會致使類的初始化。如下狀況類被初始化:安全

類初始化的一些規則:多線程

  1. 類從頂到底的順序初始化,因此聲明在頂部的字段遭遇底部的字段初始化;
  2. 超類早於子類和衍生類的初始化;
  3. 若是類的初始化是因爲訪問靜態域而觸發,那麼只能聲明靜態域的類才被初始化,而不會觸發超類的初始化或者子類的初始化,即便靜態域被子類或子接口或者它的實現類鎖引用;
  4. 接口初始化不會致使父接口的初始化;
  5. 靜態域的初始化時在類的靜態初始化期間,非靜態域的初始化是在類的實例建立期間,這意味着靜態域初始化在非靜態域以前;
  6. 非靜態域經過構造器初始化,子類在作任何初始化以前構造器會先調用父類的構造器,它保證了父類非靜態或實例變量初始化早於子類;

3、單例模式的特色性能

單例模式有如下特色:spa

  1. 單例類只能有一個實例。
  2. 單例類必須本身建立本身的惟一實例。
  3. 單例類必須給全部其餘對象提供這一實例。

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

4、餓漢式單例線程

public class Singleton {
  private Singleton() {}
  private static final Singleton single = new Singleton();
  //靜態工廠方法 
  public static Singleton getInstance() {
      return single;
  }
}

由於這自己就是static修飾的方法,因此是在類加載的時候被建立,後期不會再改變,因此線程是安全的。設計

5、懶漢式單例

public class SingletonTest {
    public static SingletonTest singleton = null;
    public static SingletonTest getInstance(){
        if(singleton == null){
            singleton = new SingletonTest();
            System.out.println("建立一次");
        }
        return singleton;
    }
    public void show(){
        System.out.println("我是江疏影");
    }

    public static void main(String[] args) {
        SingletonTest singleton = SingletonTest.getInstance();
        SingletonTest singleton1 = SingletonTest.getInstance();
        singleton.show();
        singleton1.show();
        if(singleton==singleton1){
            System.out.println("該對象的字符串表示形式:");
            System.out.println("singleton :"+singleton.toString());
            System.out.println("singleton1:"+singleton1.toString());
        }
    }
}

懶漢式方法老是會出現這樣或那樣的問題的,由於考慮到了多線程機制,實現起來比較麻煩,而且還會出現問題,就算是使用了必定的解救辦法(同步、加鎖、雙重判斷)的辦法,性能仍是被損耗了,所以懶漢式方法的弊端很是大。

6、雙檢鎖/雙重校驗鎖

描述:採用雙鎖機制,安全且在多線程狀況下能保持高性能。多線程安全

package designMode;

public class Singleton {
    private volatile static Singleton singleton;
    public static synchronized Singleton getSingleton(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

或者使用以下方式,雙重判斷,第二次判斷就是防止已經有一個對象產生了,所以也能夠達到相應的目的。

package designMode;

public class Singleton {
    private volatile static Singleton singleton;
    public static Singleton getSingleton(){
        if(singleton==null){
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

這裏的兩次判斷,第一判斷:效率,第二判斷:避免同步。之因此這樣是由於避免加鎖後,再次加鎖。大大加強了執行效率。

7、枚舉實現單例

一、枚舉單例(Enum Singleton)在Effective Java一書中提到,由於其功能完善,使用簡潔,無償的提供了序列化機制,在面對複雜的序列化或者反射攻擊時依然能夠絕對防止屢次實例化等優勢,被做者所推崇。

二、枚舉單例寫法簡單

如上文提到的DCL(double checked locking),實在是優勢麻煩,枚舉單例相對簡單的多。下面這段代碼就是聲明枚舉實例的一般作法,它可能還包含實例變量和實例方法,枚舉單例是線程安全的。

public enum  DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum(){
        connection = new DBConnection();
    }
    public DBConnection getConnection(){
        return connection;
    }
}
public class Test {
    public static void main(String[] args) {
        DBConnection conn1 = DataSourceEnum.DATASOURCE.getConnection();
        DBConnection conn2 = DataSourceEnum.DATASOURCE.getConnection();
        System.out.println(conn1 == conn2);
    }
}

8、靜態內部類實現單例

package designMode.singleton;

public class Singleton {
    private static class SingleTonHoler{
        private static Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingleTonHoler.INSTANCE;
    }
}

一、靜態內部類實現單例的優勢是:外部類加載時並不須要當即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。即當SingleTon第一次被加載時,並不須要去加載SingleTonHoler,只有當getInstance()方法第一次被調用時,纔會去初始化INSTANCE,第一次調用getInstance()方法會致使虛擬機加載SingleTonHoler類,這種方法不只能確保線程安全,也能保證單例的惟一性,同時也延遲了單例的實例化。

二、當getInstance()方法被調用時,SingleTonHoler纔在SingleTon的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象INSTANCE也真正被建立,而後再被getInstance()方法返回出去,這點同餓漢模式。那麼INSTANCE在建立過程當中又是如何保證線程安全的呢?在《深刻理解java虛擬機》中,有這麼一句話:

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其它線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是一個類的<clinit>()方法中有耗時很長的操做,就有可能形成多個進程阻塞(須要注意的是,其它線程雖然會被阻塞,但若是執行<clinit>()方法後,其它線程喚醒以後不會再次進入<clinit>()方法。同一個加載器下,一個類型只會初始化一次。)在實際應用中,這種阻塞每每是隱蔽的。

<clinit>()方法簡介:

一、先理解類初始化階段的含義:該階段負責爲類變量賦予正確的初始值,是一個類或接口被首次使用前的最後一項工做。

二、<clinit>()方法的執行時期:類初始化階段(該方法只能被JVM調用,專門承擔類變量的初始化工做)。

三、<clinit>()方法的內容:全部的類變量初始化語句和類型的靜態初始化器。

四、類的初始化時機:

  • 首次建立某個類的新實例時(new,反射,克隆,反序列化);
  • 首次調用某個類的靜態方法時;
  • 首次使用某個類或接口的靜態字段或對該字段(final字段除外)賦值時;
  • 首次調用java的某些反射方法時;
  • 首次初始化某個類的子類時;
  • 首次在虛擬機啓動時某個含有main()方法的那個啓動類。

五、注意:並不是全部的類都擁有一個<clinit>()方法,知足下列條件之一的類不會擁有<clinit>()方法:

  • 該類沒有聲明任何類變量,也沒有靜態初始化語句;
  • 該類聲明瞭類變量,但沒有明確使用類變量初始化語句或靜態初始化語句初始化;
  • 該類僅包含靜態final變量的類變量初始化語句,而且類變量初始化語句是編譯時常量表達式。

 

故而,能夠看出INSTANCE在建立過程當中是線程安全的,因此說靜態內部類形式的單例可保證線程安全,也能保證單例的惟一性,同時也延遲了單例的實例化。

那麼,是否是能夠說靜態內部類實現單例模式就是完美的了呢?其實否則,靜態內部類實現單例也有一個致命的缺點,就是傳參的問題,因爲靜態內部類的形式建立單例,故而沒法傳遞參數進去,例如Contxt這種參數,因此咱們建立單例時,能夠根據實際狀況,進行斟酌。

9、總結

在這個單例模式中,我但願你們不要只知道單例的思想,更要知道類的加載和初始化時機,以及多線程的機制,我想這纔是真正有意義的呢。枚舉單例有序列化和線程安全的保證,並且實現簡單,是實現單例最好的方式。

 

素小暖講設計模式@目錄

相關文章
相關標籤/搜索