設計模式之一----單例模式
阿新 • • 發佈:2019-01-10
在一個系統裡,對於某些物件,有且只能有一個,當我們有這樣的需求的時候,就需要用到單例模式了。
單例模式分為懶漢模式和餓漢模式。我們使用程式碼來進行說明:
首先建立一個用到了餓漢模式的Singleton類在com.single下:
package com.single; /* * 單例模式 * 場合:保證有些物件有且只有一個 * 型別:餓漢模式 * */ public class Singleton { //1、將構造方法私有化 不允許外部直接建立物件 private Singleton(){} //2、類的唯一例項 只要類建立 該例項就會被建立 private static Singleton instance = new Singleton(); //3、提供一個用於獲取例項的方法 public static Singleton getInstance(){ return instance;//類一被建立 該例項就會被建立 } }
然後建立一個用到了懶漢模式的Singleton2在com.single下:
最後寫一個測試類Test在com.single下:package com.single; public class Singleton2 { //1、將構造方法私有化 private Singleton2(){} //2、宣告類的唯一例項 private static Singleton2 instance;//類載入的時候沒有生成這個例項 而是使用者獲取來才生成這個例項 //3、提供一個用於獲取例項的方法 public static Singleton2 getInstance(){ if(instance == null){//僅當第一次嘗試獲取例項的時候才生成例項 instance = new Singleton2(); } return instance; } }
package com.single; public class Test { public static void main(String[] args) { //餓漢模式 Singleton s1 = Singleton.getInstance();//獲取例項 Singleton s2 = Singleton.getInstance();//獲取例項 System.out.println(s1==s1);//true 同一個例項 無論獲取多少次都是同一個物件 //懶漢模式 Singleton2 s3 = Singleton2.getInstance(); Singleton2 s4 = Singleton2.getInstance(); System.out.println(s3==s4);//true 也是同一個例項 //懶漢模式與餓漢模式的區別: //餓漢模式載入類的速度比較慢,但是允許時獲取物件速度比較快 執行緒安全 //懶漢模式載入類比較快 但是執行時獲取物件的速度比較慢 執行緒不安全 } }
前面我們提到了懶漢模式是執行緒不安全的,所以我們能不能想辦法做成執行緒安全的呢? 來嘗試一下:
package com.single;
public class Singleton2 {
private static Object lock = new Object();//生成一把鎖
//1、將構造方法私有化
private Singleton2(){}
//2、宣告類的唯一例項
private static Singleton2 instance;//類載入的時候沒有生成這個例項 而是使用者獲取來才生成這個例項
//3、提供一個用於獲取例項的方法
public static Singleton2 getInstance(){
synchronized(lock){//加上鎖
if(instance == null){//僅當第一次嘗試獲取例項的時候才生成例項
instance = new Singleton2();
}
}
return instance;
}
}
這樣做是不是彷彿沒什麼問題了,但是我們要知道實際上加鎖是開銷特別大的,除了第一次生命單例以外我們用不到每次都加鎖,所以我們採用雙重校驗鎖:
package com.singleton;
public class Singleton2 {
private Singleton2(){};
private static Singleton2 instance;
private static Object lock = new Object();
public static Singleton2 getInstance(){
if(instance == null){//只有第一次建立例項才加鎖
synchronized(lock){//加鎖實際上很浪費時間
if(instance == null){
instance = new Singleton2();
System.out.println("建立成功");
}
}
}
return instance;
}
public void save(){
System.out.println("儲存成功");
}
}
彷彿沒什麼問題了是吧,但是由於JVM指令重排優化特性的存在,導致初始化Singleton和將物件地址賦給instance欄位的順序可能是不確定的,在某個執行緒建立單例物件時,在構造方法被呼叫之前,就為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance,取到的就是狀態不正確的物件,程式就會出錯。然而volatile關鍵字除了保障變數被多執行緒訪問的安全性的同時,還可以禁止JVM重排優化,所以這個地方最好在instance前加上volatile關鍵字:
package com.singleton;
public class Singleton2 {
private Singleton2(){};
private static volatile Singleton2 instance;//懶漢模式 預設執行緒不安全 慢
private static Object lock = new Object();
public static Singleton2 getInstance(){
if(instance == null){
synchronized(lock){//加鎖實際上很浪費時間
if(instance == null){
instance = new Singleton2();
System.out.println("建立成功");
}
}
}
return instance;
}
public void save(){
System.out.println("儲存成功");
}
}
然而現在在不需要繼承這個單例物件對應的類的時候有一種最簡單,也絕對執行緒安全的方式:單列舉,詳情點選連結