單例模式不管是在實際項目開發仍是面試中,都是常常會涉及到,今天總結一下什麼樣的單例模式纔是正確的。java
/** * 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的代碼中間加入線程休眠時間,這樣後運行的線程才能拿到先運行線程建立的單例對象。程序員
/** * 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() 方法。可是同步操做只須要在第一次調用時才被須要,即第一次建立單例實例對象時。面試
/** * 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
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() 以前必須調用某個方法設置參數給它,那樣這種單例寫法就沒法使用了。安全
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
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(); 複製代碼
先看一下枚舉類型的實質: 咱們定義一個表明不一樣顏色的枚舉類型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; } } 複製代碼