深度講解23種設計模式,力爭每種設計模式都刨析到底。廢話不多說,開始第一種設計模式 - 單例。 作者已知的單例模式有8種寫法,而每一種寫法,都有自身的優缺點。 1,使用頻率最高的寫法,廢話不多說,直接上代碼 /** * @author xujp * 餓漢式 靜態變數 單例 */ public cla ...
深度講解23種設計模式,力爭每種設計模式都刨析到底。廢話不多說,開始第一種設計模式 - 單例。
作者已知的單例模式有8種寫法,而每一種寫法,都有自身的優缺點。
1,使用頻率最高的寫法,廢話不多說,直接上代碼
/**
* @author xujp
* 餓漢式 靜態變數 單例
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return instance;
}
private String tmp;
public String getTmp() {
return tmp;
}
public void setTmp(String tmp) {
this.tmp = tmp;
}
}
new Singleton() 的執行時機 - > 類載入時
這種方法是最通用的單例實現,也是筆者常用的,但這種方法有一些缺點:
1)記憶體方面,如果單例中的內容很多,會在類載入時,就占用java虛擬機(這裡專指HotSpot)空間。
2)序列化以及反序列化問題,如果這個單例類實現了序列化介面Serializable,那麼可以通過反序列化來破壞單例。
通過反序列化破壞單例:
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton=null;
Singleton singletonNew=null;
singleton=Singleton.getSingleton();
singleton.setTmp("123");
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(singleton);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
singletonNew= (Singleton) ois.readObject();
singleton.setTmp("456");
System.out.println(singletonNew.getTmp());
System.out.println(singleton.getTmp());
System.out.println(singleton==singletonNew);
}
輸出結果為:
false
123
456
從這裡例子中我們可以看到單例被破壞了,也就不能保證單例的唯一性。
2,第一種方案的變種
/**
* @author xujp
* 餓漢式 靜態代碼塊 單例
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private final static Singleton instance;
static {
instance = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return instance;
}
}
其實這種方法和第一種方法,幾乎沒有什麼區別。
3,線程不安全的寫法 - 1
/**
* @author xujp
* 懶漢式 單例 線程不安全
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance;
private Singleton(){}
public static Singleton getSingleton(){
if(null != instance) {
instance = new Singleton();
}
return instance;
}
}
這種寫法,雖然實現了懶載入,節省了記憶體,但線程不安全。
假設有兩個線程,並假設 new Singleton() 耗時2秒,0秒時,線程1執行new,然後去等待,1秒時,線程2執行if判斷,
這個時候判斷結果就是true,這樣就會出現兩個Singleton對象,完美破壞掉了單例。
4,線程不安全的寫法 - 2
/**
* @author xujp
* 懶漢式 單例 代碼塊加鎖 線程不安全
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance;
private Singleton(){}
public static Singleton getSingleton(){
if(null != instance) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
這種寫法雖然在new Single()時,增加了鎖,但這個鎖,並不能阻止單例被破壞,所以這種寫法錯誤。
同樣,假設有兩個線程,線程1執行到synchronized時,線程2執行if判斷,這個時候判斷結果就是true,
這樣就會出現兩個Singleton對象,同樣完美破壞掉了單例。
5,線程安全,但資源消耗過多
/**
* @author xujp
* 懶漢式 單例 方法加鎖 線程不安全
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance;
private Singleton(){}
synchronized public static Singleton getSingleton(){
if(null != instance) {
instance = new Singleton();
}
return instance;
}
}
這種寫法確實能夠保證線程安全,但synchronized屬於方法鎖,而方法鎖回鎖定對象,導致性能低下。
6,相對完美的寫法 - 1
/**
* @author xujp
* 懶漢式 單例 代碼加鎖 線程安全
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance;
private Singleton(){}
public static Singleton getSingleton(){
if(null != instance) {
synchronized (Singleton.class) {
if(null != instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
雙檢查這種寫法,在多線程問題上,屬實沒有問題,synchronized也沒有鎖定對象,而且也優化了鎖資源開銷問題。
7,相對完美的寫法 - 2
/**
* @author xujp
* 懶漢式 單例 靜態內部類 線程安全
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static class SingletonInstance{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return SingletonInstance.instance;
}
}
使用靜態內部類來實現單例,主要藉助JVM機制,靜態內部類初始化的時候,其他線程無法進入,從而避免了多線程問題。
而且靜態內部類不會直接初始化,從而減輕了記憶體開銷。
8,完美寫法
/**
* @author xujp
* 枚舉實現單例
*/
public enum Singleton {
SINGLETON;
private String property = "hello ca fe ba be";
public void doSomeThing(){
System.out.println(property);
}
}
這種寫法用枚舉解決多線程問題,而且時唯一一種解決序列化問題的寫法。
改寫法出自大神Josh Bloch,如果有興趣可以去查看一下他的資料。
總結:
1,1和2寫法雖然是餓漢式,沒有實現懶載入,也沒有100%保證單例,但卻是我們最常用的寫法,
因為,單例對象通常占用空間不會很大,而且程式都由程式員自己管理,被反序列的危險性不高。
2,3和4寫法實現了懶載入,減少了記憶體開銷,但不能使用,因為多線程開發,是我們常見的開發。
3,5寫法使用了方法鎖,會將對象鎖住,會導致性能大打折扣。
4,6和7寫法,懶載入、性能都非常完美,缺點只有一個,那就是序列化問題。
5,8寫法,筆者暫未發現缺點。
實際開發中,無論是使用1、2寫法,還是使用6、7寫法,亦或是使用8寫法,都是可以的。