單例模式 什麼是單例? 應用場景 代碼實現 餓漢式 中國古代神話中有女媧補天一說,現在天破了,我們去求女媧補天。 女媧用英語來說是 A Goddess In Chinese Mythology,意思就是神話中的女神,女媧是獨一無二的,現在我們就建一個女神類Goddess。 神話中,我們都是女媧造出來 ...
單例模式
什麼是單例?
- 該類只能有一個實例。
- 該類負責創建自己的對象。
- 在整個項目中都能訪問到這個實例。
應用場景
- 讀配置文件時,維護全局的Config類。
- 線程池、連接池,統一進行分配和管理。
- 網站的計數器,也可以採用單例模式實現,保持同步
代碼實現
餓漢式
中國古代神話中有女媧補天一說,現在天破了,我們去求女媧補天。
女媧用英語來說是 A Goddess In Chinese Mythology,意思就是神話中的女神,女媧是獨一無二的,現在我們就建一個女神類Goddess。
1 public class Goddess {
2
3 }
神話中,我們都是女媧造出來的,人是不能造女媧的,所以要女媧私有化構造。
1 public class Goddess {
2 private Goddess(){};//私有化構造
3 }
既然人不能女媧,那女媧是怎麼來的,女媧伴隨天地初開產生的,所以要自己創建對象。
1 public class Goddess {
2 private static final Goddess goddess = new Goddess();//自己創建對象
3 private Goddess(){};//私有化構造
4 }
女媧是神秘的,凡胎肉眼看不到所以要private,static保證了女媧伴隨天地初開,在記憶體中永生,不能被垃圾回收器回收。final保證女媧是不會變的,永遠是那個女神,嘖...嘖...
1 public class Goddess {
2 private static final Goddess goddess = new Goddess();//自己創建對象
3 private Goddess(){};//私有化構造
4 public static Goddess getInstance(){//獲取唯一可用的對象
5 return goddess;
6 }
7 }
既然單例不能被實例化,就需要一個靜態的方法來獲取對象。這是單例的“餓漢式”,代碼第二行就產生了女媧。
我們來驗證一下:
1 public class GoddessTest {
2 public static void main(String[] args){
3 Goddess goddes1 = Goddess.getInstance();
4 Goddess goddes2 = Goddess.getInstance();
5 System.out.println("兩個對象的引用是否相等:"+(goddes1==goddes2));
6 }
7 }
結果:
兩個對象的引用是否相等:true
兩個對象的引用是同一個對象,說明我們實現了單例模式。
懶漢式
1 public class Goddess {
2 private static Goddess goddess ;//此時不創建對象
3 private Goddess(){};//私有化構造
4 public static Goddess getInstance(){//獲取唯一可用的對象
5
6 if (goddess == null) {//如果沒有女媧才去創建
7 goddess = new Goddess();
8 }
9 return goddess;
10 }
11 }
女媧比較懶,沒事幹的時候,處於混沌狀態,第一次求女媧時,女媧才會實例化。
好處:省了一段時間的記憶體,
壞處:第一次請女媧會慢,因為要消耗CPU去實例化。多線程下也有問題,如果多個人同時請女媧,會產生很多女媧,不是線程安全。
我們加上synchronized進行優化,線程調用前必須獲取同步鎖,調用完後會釋放鎖給其他線程用,也就是請女媧必須排隊,大家一個一個來。
1 public class Goddess {
2 private static Goddess goddess ;//此時不創建對象
3 private Goddess(){};//私有化構造
4 public static synchronized Goddess getInstance(){//獲取唯一可用的對象
5
6 if (goddess == null) {//如果沒有女媧才去創建
7 goddess = new Goddess();
8 }
9 return goddess;
10 }
11 }
然而,這樣多個人都要去搶鎖,即使對象已經實例化,沒有充分利用CPU資源併發優勢(特別是多核情況)。
我們把synchronized放到方法體內,如果女媧還沒實例化,才回去搶鎖,這就極大的利用了CPU資源。
代碼如下:
雙檢鎖/雙重校驗鎖
1 //這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。
2 public class Goddess {
3 private volatile static Goddess goddess ;//此時不創建對象
4 private Goddess(){};//私有化構造
5 public static Goddess getInstance(){//獲取唯一可用的對象
6 if (goddess == null) {//如果女媧還沒有實例化,請女媧的人進行等待,準備搶奪請求鎖。
7 synchronized(Goddess.class){
8 if (goddess == null) {//第一個搶過之後,後面的人也不用等待了,女媧還有五秒到達戰場
9 goddess = new Goddess();
10 }
11 }
12 }
13 return goddess;
14 }
15 }
枚舉模式
1 public enum Goddess {
2 INSTANCE;
3 public Goddess getInstance(){
4 return INSTANCE;
5 }
6 }
以上方式是在不考慮反射和序列化的情況下實現的,如果考慮了反射,就無法做到單例類只能有一個實例這種說法了。但是枚舉單例模式可以避免這兩種情況。
我們以餓漢式為例:
1 public class GoddessTest {
2 public static void main(String[] args) throws Exception {
3 Goddess goddes1=Goddess.getInstance();
4 Goddess goddes2=Goddess.getInstance();
5 Constructor<Goddess> constructor=Goddess.class.getDeclaredConstructor();
6 constructor.setAccessible(true);
7 Goddess goddes3=constructor.newInstance();
8 System.out.println("正常情況下,兩個對象的引用是否相等:"+(goddes1 == goddes2));
9 System.out.println("反射攻擊時,兩個對象的引用是否相等:"+(goddes1 == goddes3));
10 }
11 }
結果:
1 正常情況下,兩個對象的引用是否相等:true
2 反射攻擊時,兩個對象的引用是否相等:false
枚舉單例下示例:
1 public class GoddessTest {
2 public static void main(String[] args) throws Exception {
3 Goddess goddes1=Goddess.INSTANCE;
4 Goddess goddes2=Goddess.INSTANCE;
5 Constructor<Goddess> constructor= null;
6 constructor = Goddess.class.getDeclaredConstructor();
7 constructor.setAccessible(true);
8 Goddess goddes3= null;
9 goddes3 = constructor.newInstance();
10 System.out.println("正常情況下,兩個對象的引用是否相等:"+(goddes1 == goddes2));
11 System.out.println("反射攻擊時,兩個對象的引用是否相等:"+(goddes1 == goddes3));
12 }
13 }
結果:
1 Exception in thread "main" java.lang.NoSuchMethodException: xxx.xxx.Goddess.<init>()
2 at java.lang.Class.getConstructor0(Class.java:3082)
3 at java.lang.Class.getDeclaredConstructor(Class.java:2178)
4 at com.slw.design.danli.GoddessTest.main(GoddessTest.java:11)
反射通過newInstance創建對象時,會檢查該類是否enum修飾,如果是則拋出異常,反射失敗。所以枚舉是不怕發射攻擊的。