在上一篇文章 單例模式(上)---如何優雅地保證執行緒安全問題中,我們採取了懶漢式寫法來寫我們的單例模式,並且重點講解了懶漢式中執行緒安全的問題。這篇我們來講講單例模式中的其他幾種寫法。
上篇文章中,方法和變數的宣告都忘了加上“static”的宣告,這裡提醒一下。
懶漢式
懶漢式在上節我們已經講過了,直接給出程式碼:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton(){};
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複製程式碼
懶漢式這種方式需要我們來自己加鎖,保證執行緒安全的問題。
不過就算我們保證了執行緒安全,這種寫法還是無法保證存在唯一一個物件例項。因為別人還是可以通過反射的方式來建立一個新的物件。我寫個示例:
public class Singleton {
public static void main(String[] args) throws Exception{
//獲得構造器
Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
//把構造器設定為可訪問
c.setAccessible(true);
//建立兩個例項物件
Singleton s1 = c.newInstance();
Singleton s2 = c.newInstance();
//比較下兩個例項是否相等
System.out.println(s1 == s2);
}
}
複製程式碼
列印結果:false。
所以懶漢式這種方式還是存在一些缺點的。
餓漢式
所謂餓漢式,就是一開始把物件例項建立出來,而不是等getInstance這個方法被呼叫才來建立物件。程式碼如下:
public class Singleton2 {
private static Singleton2 instance = new Singleton2();
//私有構造器
private Singleton2(){};
public static Singleton2 getInstance() {
return instance;
}
}
複製程式碼
餓漢式與懶漢式相比,我們不用管執行緒安全的問題,程式碼看起來也比較簡潔。
但是,由於物件一開始就被建立出來了,假如我們從頭到尾都不呼叫getInstance()這個方法,那麼這個物件就白建立了。
當然,和懶漢式一樣,餓漢式也存在反射問題。
總結一下餓漢式的一些問題:
1、有可能出現物件白白浪費的情況。
2、和懶漢式一樣,無法組織反射問題。
採用靜態內部類的寫法
直接上程式碼
public class Singleton3 {
//靜態內部類
private static class LazyHolder{
private static Singleton3 instance = new Singleton3();
}
//私有構造器
private Singleton3(){};
public static Singleton3 getInstance() {
return LazyHolder.instance;
}
}
複製程式碼
由於外部類無法訪問靜態內部類,因此只有當外部類呼叫Singleton.getInstance()方法的時候,才能得到instance例項。
並且,instance例項物件初始化的時機並不是在Singleton被載入的時候,而是當getInstance()方法被呼叫的時候,靜態內部類才會被載入,這時instance物件才會被初始化。並且也是執行緒安全的。
所以,與餓漢式相比,通過靜態內部類的方式,可以保證instance例項物件不會被白白浪費。
但是,它仍然存在反射問題。
採取列舉的方式
直接上程式碼:
public enum Singleton4 {
//一般用大寫的了,不過為了和前面的統一
//我就用小寫的了
instance;
}
複製程式碼
列舉的方式簡單吧?一行程式碼就搞定了,不過和餓漢式一樣,由於一開始instance例項就被建立了,所以有可能出現白白浪費的情況。
但是,通過列舉的方式,不僅程式碼簡單,執行緒安全,而且JVM還能阻止反射獲取列舉類的私有構造器。
下面做個實驗
public enum Singleton4 {
//一般用大寫的了,不過為了和前面的統一
//我就用小寫的了
instance;
public static void main(String[] args) throws Exception{
//獲得構造器
Constructor<Singleton4> c = Singleton4.class.getDeclaredConstructor();
//把構造器設定為可訪問
c.setAccessible(true);
//建立兩個例項物件
Singleton4 s1 = c.newInstance();
Singleton4 s2 = c.newInstance();
//比較下兩個例項是否相等
System.out.println(s1 == s2);
}
}
複製程式碼
結果出現了異常:
Exception in thread "main" java.lang.NoSuchMethodException: singleton.Singleton4.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at singleton.Singleton4.main(Singleton4.java:12)
複製程式碼
所以,這種列舉的方式可以說的用的最多的一種方式了,唯一的缺點就是物件一開始就被建立,可能出現白白浪費沒有用到物件的情況。
不過,總體上,還是推薦採用列舉的方式來寫。
完
獲取更多原創文章,可以關注下我的公眾號:苦逼的碼農,我會不定期分享一些資源和軟體等。同時也感謝把文章介紹給更多需要的人。