關於單例模式

單例模式不管是在實際項目開發仍是面試中,都是常常會涉及到,今天總結一下什麼樣的單例模式纔是正確的。java

1. 存在問題的單例模式

1.1 線程不安全的懶漢式

/** * Created by zhoujunfu on 2016/8/24. * 線程不安全的懶漢式單例 */
class SingletonLazyNonThreadSafe {

    private static SingletonLazyNonThreadSafe instance;

    private SingletonLazyNonThreadSafe() {
        System.out.println("初始化單例對象:" + this.hashCode());
    }

    public static SingletonLazyNonThreadSafe getInstance() {
        if (instance == null) {
            instance =  new SingletonLazyNonThreadSafe();
        }

        System.out.println("獲取單例對象:" + instance.hashCode());
        return instance;
    }

}

class Runner implements Runnable {

    @Override
    public void run() {
        SingletonLazyNonThreadSafe.getInstance();
    }
}

public class SingletonDemo {

    public static void main(String[] args) throws InterruptedException {
        // 兩個線程併發訪問單例類建立實例
        Runner runnerOne = new Runner();
        Runner runnerTwo = new Runner();

        Thread threadOne = new Thread(runnerOne);
        Thread threadTwo = new Thread(runnerTwo);

        threadOne.start();
        threadTwo.start();
    }

}
複製代碼

懶漢式,也是最想固然的單例方式,線程不安全,能夠從如下運行結果看出,線程併發訪問這種單例類時,會初始化多個實例,違反了單例類的原則,若是在兩個線程start的代碼中間加入線程休眠時間,這樣後運行的線程才能拿到先運行線程建立的單例對象。程序員

1.2 線程安全的懶漢式

/** * Created by zhoujunfu on 2016/8/24. * 懶漢式單例 */
class SingletonLazyThreadSafe {

    private static SingletonLazyThreadSafe instance;

    private SingletonLazyThreadSafe() {
        System.out.println("初始化單例對象:" + this.hashCode());
    }

    public static synchronized SingletonLazyThreadSafe getInstance() {
        if (instance == null) {
            instance =  new SingletonLazyThreadSafe();
        }

        System.out.println("獲取單例對象:" + instance.hashCode());
        return instance;
    }

}
class Runner implements Runnable {

    @Override
    public void run() {
        SingletonLazyThreadSafe.getInstance();
    }
}

public class TestSingleton {

    public static void main(String[] args) throws InterruptedException {
        // 兩個線程併發訪問單例類建立實例
         Runner runnerOne = new Runner();
        Runner runnerTwo = new Runner();

        Thread threadOne = new Thread(runnerOne);
        Thread threadTwo = new Thread(runnerTwo);

        threadOne.start();
        threadTwo.start();
    }

}
複製代碼

經過將整個getInstance方法設爲同步的,來保證每次只能有一個線程進入到建立/獲取實例的方法內,雖然作到了線程安全,而且解決了多實例的問題,可是它並不高效。由於在任什麼時候候只能有一個線程調用 getInstance() 方法。可是同步操做只須要在第一次調用時才被須要,即第一次建立單例實例對象時。面試

1.3 雙重檢驗鎖

/** * Created by zhoujunfu on 2016/8/24. * 懶漢式雙重檢查鎖 */
class SingletonDoubleCheck {
    private SingletonDoubleCheck() {
        System.out.println("初始化單例對象:" + this.hashCode());
    }

    private static SingletonDoubleCheck instance;

    public static SingletonDoubleCheck getInstance() {
        if (instance == null) {
            synchronized (SingletonDoubleCheck.class) {
                if (instance == null) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}
複製代碼

雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其爲雙重檢查鎖,由於會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。爲何在同步塊內還要再檢驗一次?由於可能會有多個線程一塊兒進入同步塊外的 if,若是在同步塊內不進行二次檢驗的話就會生成多個實例了。
這段代碼看起來很完美,很惋惜,它是有問題。主要在於instance = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
1.給 instance 分配內存
2.調用 Singleton 的構造函數來初始化成員變量
3.將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)
可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
咱們只須要將 instance 變量聲明成 volatile 就能夠了。有些人認爲使用 volatile 的緣由是可見性,也就是能夠保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但實際上是不對的。使用 volatile 的主要緣由是其另外一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。好比上面的例子,取操做必須在執行完 1-2-3 以後或者 1-3-2 以後,不存在執行到 1-3 而後取到值的狀況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操做都先行發生於後面對這個變量的讀操做(這裏的「後面」是時間上的前後順序)。
可是特別注意在 Java 5 之前的版本使用了 volatile 的雙檢鎖仍是有問題的。其緣由是 Java 5 之前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能徹底避免重排序,主要是 volatile 變量先後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,因此在這以後才能夠放心使用 volatile。sql

2. 不存在問題的單例模式

2.1 餓漢式(非懶加載)

class SingletonHungry {
    private SingletonHungry() {
        System.out.println("初始化單例對象:" + this.hashCode());
    }

    private static SingletonHungry instance = new SingletonHungry();

    public SingletonHungry getInstance() {
        return instance;
    }
}
複製代碼

這種方法很是簡單,由於單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,因此建立實例自己是線程安全的。 這種寫法若是完美的話,就不必在囉嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類後一開始就被初始化,即便客戶端沒有調用 getInstance()方法。餓漢式的建立方式在一些場景中將沒法使用:譬如 Singleton 實例的建立是依賴參數或者配置文件的,在 getInstance() 以前必須調用某個方法設置參數給它,那樣這種單例寫法就沒法使用了。安全

2.2 餓漢式(懶加載)

class SingletonStaticNestedClass {
    private SingletonStaticNestedClass() {
    }

    private static class Holder {
        private static final SingletonStaticNestedClass instance = new SingletonStaticNestedClass();
    }

    public SingletonStaticNestedClass getInstance() {
        return Holder.instance;
    }
}
複製代碼

這種寫法仍然使用JVM自己機制保證了線程安全問題;因爲 Holder 是私有的,除了 getInstance() 以外沒有辦法訪問它,所以它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本,但反序列化時會出現問題。markdown

2.3 枚舉式(終極方法)

enum SingletonByEnum {
    INSTANCE;
}
複製代碼

咱們能夠經過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。建立枚舉默認就是線程安全的,因此不須要擔憂double checked locking,並且還能防止反序列化致使從新建立新的對象。可是仍是不多看到有人這樣寫,多是由於不太熟悉吧。 網絡上不少關於單例類的文章都介紹了用枚舉法實現單例,但僅僅靠上述的例子還沒法知道具體的使用方法,下面以一個具體的例子來講明如何經過枚舉實現單例類。網絡

//Example 1
public enum MyDataBaseSource {
    DATASOURCE;
    private ComboPooledDataSource cpds = null;

    private MyDataBaseSource() {
        try {

            /*--------獲取properties文件內容------------*/
            // 方法一:
            /* * InputStream is = * MyDBSource.class.getClassLoader().getResourceAsStream("jdbc.properties"); * Properties p = new Properties(); p.load(is); * System.out.println(p.getProperty("driverClass") ); */

            // 方法二:(不須要properties的後綴)
            /* * ResourceBundle rb = PropertyResourceBundle.getBundle("jdbc") ; * System.out.println(rb.getString("driverClass")); */

            // 方法三:(不須要properties的後綴)
            ResourceBundle rs = ResourceBundle.getBundle("jdbc");
            cpds = new ComboPooledDataSource();
            cpds = new ComboPooledDataSource();
            cpds.setDriverClass(rs.getString("driverClass"));
            cpds.setJdbcUrl(rs.getString("jdbcUrl"));
            cpds.setUser(rs.getString("user"));
            cpds.setPassword(rs.getString("password"));
            cpds.setMaxPoolSize(Integer.parseInt(rs.getString("maxPoolSize")));
            cpds.setMinPoolSize(Integer.parseInt(rs.getString("minPoolSize")));
            System.out.println("-----調用了構造方法------");
            ;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() {
        try {
            return cpds.getConnection();
        } catch (SQLException e) {
            return null;
        }
    }

}
public class Test {
    public static void main(String[] args) {
        MyDataBaseSource.DATASOURCE.getConnection() ;
        MyDataBaseSource.DATASOURCE.getConnection() ;
        MyDataBaseSource.DATASOURCE.getConnection() ;
    }
}
//Example 2
public enum UserActivity {
    INSTANCE;

    private DataSource _dataSource;
    private JdbcTemplate _jdbcTemplate;

    private UserActivity() {
        this._dataSource = MysqlDb.getInstance().getDataSource();
        this._jdbcTemplate = new JdbcTemplate(this._dataSource);
    }

    public void dostuff() {
     ...
    }
}

// use it as ...
UserActivity.INSTANCE.doStuff();
複製代碼

Tips: 關於枚舉

先看一下枚舉類型的實質: 咱們定義一個表明不一樣顏色的枚舉類型Color,併發

public enum Color {
    RED, BLUE, GREEN;
}
複製代碼

除了以上的定義方式,咱們還能夠以下定義,ide

public enum Color {
    RED(), BLUE(), GREEN();
}
複製代碼

到這裏你就會以爲迷茫(若是你是初學者的話),爲何這樣子也能夠?其實,枚舉的成員就是枚舉對象,只不過他們是靜態常量而已。使用 javap 命令(javap 文件名<沒有後綴.class>)能夠反編譯 class 文件,以下函數

咱們可使用普通類來模擬枚舉,下面定義一個 Color 類。

public class Color {
    private static final Color RED = new Color();
    private static final Color GREEN = new Color();
    private static final Color BLUE = new Color();
}
複製代碼

對比一下,你就明白了。若是按照這個邏輯,是否還能夠爲其添加另外的構造方法?答案是確定的!

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    String desc;
    int value;
}
複製代碼

爲 Color 聲明瞭兩個成員變量,併爲其構造帶參數的構造器。若是你這樣建立一個枚舉

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);
}
複製代碼

編譯器就會報錯,由於沒有對應的構造函數。 對於類來說,最好將其成員變量私有化,而後,爲成員變量提供 get、set 方法。按照這個原則,能夠進一步寫好 enum Color.

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
複製代碼

可是,java 設計 enum 的目的是提供一組常量,方便用戶設計。若是咱們冒然的提供 set 方法(外界能夠改變其成員屬性),好像是有點違背了設計的初衷。那麼,咱們應該捨棄 set 方法,保留 get 方法。

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
複製代碼

普通類,咱們能夠將其實例化,那麼,可否實例化枚舉呢?在回答這個問題以前,先來看看,反編譯以後的 Color.class 文件

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    private Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
複製代碼

能夠看出,編譯器淘氣的爲其構造方法加上了 private,那麼也就是說,咱們沒法實例化枚舉。全部枚舉類都繼承了 Enum 類的方法,包括 toString 、equals、hashcode 等方法。由於 equals、hashcode 方法是 final 的,因此不能夠被枚舉重寫(只能夠繼承)。可是,能夠重寫 toString 方法。 那麼,使用 Java 的不一樣類來模擬一下枚舉,大概是這個樣子

public class Color {
    private static final Color RED = new Color("red color", 0);
    private static final Color GREEN = new Color("green color", 1);
    private static final Color BLUE = new Color("blue color", 2);
    private static final Color YELLOW = new Color("yellow color", 3);

    private final String _name;
    private final int _id;

    private Color(String name, int id) {
        _name = name;
        _id = id;
    }

    public String getName() {
        return _name;
    }

    public int getId() {
        return _id;
    }

    public static List<Color> values() {
        List<Color> list = new ArrayList<Color>();
        list.add(RED);
        list.add(GREEN);
        list.add(BLUE);
        list.add(YELLOW);
        return list;
    }

    @Override
    public String toString() {
        return "the color _name=" + _name + ", _id=" + _id;
    }

}
複製代碼
相關文章
相關標籤/搜索