單例模式是指一個類確保其只擁有一個實例對象瞳遍,且此實例對象由類自己創(chuàng)建并提供方法為整個系統(tǒng)提供這個實例的訪問炭菌。
一滔岳、特點
- 實現(xiàn)單例模式的類只有唯一一個實例對象
- 實現(xiàn)單例模式的類必須自己創(chuàng)建此實例對象
- 實現(xiàn)單例模式的類必須向整個系統(tǒng)提供訪問此實例對象的方法
二沥潭、代碼實現(xiàn)
1. 餓漢式單例
單例類在加載時即實例化唯一對象。
package singleton;
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getSingleton() {
return singleton;
}
}
2. 懶漢式單例
單例類在系統(tǒng)首次調(diào)用其提供的訪問實例對象的方法時實例化唯一對象买窟。
package singleton;
import java.util.Objects;
public class LazySingleton {
private static LazySingleton singleton;
private LazySingleton() {}
public static LazySingleton getSingleton() {
if (Objects.isNull(singleton)) {
singleton = new LazySingleton();
}
return singleton;
}
}
相比于餓漢式單例,懶漢式單例更加合理薯定,只有需要時才創(chuàng)建具體的實例對象而非類加載時便實例化此對象始绍。為說明此問題請看以下代碼:
package singleton;
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getSingleton() {
return singleton;
}
public static void methodA() {
System.out.println("Provide other service");
}
}
在實現(xiàn)餓漢式單例的類中新增了一個方法methodA
,該方法可以提供服務话侄,當methodA
被首次調(diào)用時亏推,HungrySingleton
便被加載并實例化了靜態(tài)屬性singleton
,但實際上此實例對象尚未被使用年堆,即使后續(xù)系統(tǒng)中一直不使用singleton
實例對象吞杭,此實例對象也會常駐內(nèi)存。
3. 加鎖實現(xiàn)懶漢式單例
第 2 部分懶漢式單例代碼中存在一個問題嘀韧,沒有考慮多線程并發(fā)場景篇亭,當多個線程同時訪問getSingleton
方法時可能實例化多個對象,但最終只有一個對象被賦給靜態(tài)屬性singleton
锄贷,其它對象只能等待垃圾回收译蒂,這不免造成了系統(tǒng)資源浪費。處理線程并發(fā)問題最簡單的方法是通過加鎖實現(xiàn)線程同步谊却。
package singleton;
import java.util.Objects;
public class LazySingleton {
private static LazySingleton singleton;
private LazySingleton() {}
public static synchronized LazySingleton getSingleton() {
if (Objects.isNull(singleton)) {
singleton = new LazySingleton();
}
return singleton;
}
}
以上代碼中對懶漢式單例類提供的訪問唯一對象實例的方法添加了一個內(nèi)部鎖(當然可以將synchronized
同步塊加在方法體內(nèi)柔昼,或使用顯示鎖方案替換內(nèi)部鎖),這樣就保證同時只有一個線程能夠訪問getSingleton
方法炎辨,就不可能出現(xiàn)多個線程同時實例化對象的場景捕透。
4. 雙重鎖單例
加鎖實現(xiàn)懶漢式單例雖然解決了線程同步問題,但還存在缺陷,即singleton
在實例化成功后乙嘀,后續(xù)如果有多個線程同時調(diào)用getSingleton
方法試圖獲取singleton
的引用末购,這些線程仍需串行執(zhí)行,由此造成性能瓶頸虎谢。解決此問題的方法是采用雙重鎖單例盟榴。
package singleton;
import java.util.Objects;
public class LazySingleton {
private static LazySingleton singleton;
private LazySingleton() {}
public static LazySingleton getSingleton() {
if (Objects.isNull(singleton)) {
synchronized (LazySingleton.class) {
if (Objects.isNull(singleton)) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
雙重鎖解決了多線程獲取單例對象的性能問題,只有對象第一次實例化時會使用線程同步婴噩,一旦實例化成功擎场,后續(xù)調(diào)用getSingleton
獲取對象實例都無需再次同步執(zhí)行。
5. 增加關鍵字volatile
雙重鎖單例仍然存在發(fā)生異常的可能几莽。原因是迅办,為了提升執(zhí)行效率,Java 虛擬機可能會對synchronized
塊中代碼進行重排序章蚣。雖然singleton = new LazySingleton();
看似一個語句站欺,實際執(zhí)行過程中會分為幾個步驟完成:
- 分配內(nèi)存區(qū)域
- 在已分配內(nèi)存區(qū)域進行對象實例化
- 將實例化對象引用賦值給
singleton
Java 虛擬機在執(zhí)行過程中可能會對上訴步驟 2 和 3 進行重排序,即先將內(nèi)存區(qū)域引用賦值給singleton
然后再實現(xiàn)對象實例化究驴,如果在對象實例化前一個線程調(diào)用getSingleton
方法镊绪,則此時singleton
不為null
,但實際上對象實例化并未完成洒忧,該線程可能會拿到一個未實例化完的對象引用調(diào)用其提供的實例方法蝴韭,很明顯因為對象并未完成實例化,所以此時的實例方法調(diào)用肯定會出現(xiàn)異常熙侍。
為解決此問題榄鉴,可以在靜態(tài)屬性singleton
上加上關鍵字volatile
,這會阻止 Java 虛擬機的重排序行為蛉抓,保證對象是先實例化后再賦值給singleton
庆尘。
package singleton;
import java.util.Objects;
public class LazySingleton {
private static volatile LazySingleton singleton;
private LazySingleton() {}
public static LazySingleton getSingleton() {
if (Objects.isNull(singleton)) {
synchronized (LazySingleton.class) {
if (Objects.isNull(singleton)) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
6. 使用一個局部變量實例化對象,然后將此局部變量賦值給 singleton
除使用關鍵字volatile
外巷送,還可以通過局部變量實例化對象賦值的方法解決指令重排帶來的問題驶忌。
package singleton;
import java.util.Objects;
public class LazySingleton {
private static LazySingleton singleton;
private LazySingleton() {}
public static LazySingleton getSingleton() {
if (Objects.isNull(singleton)) {
synchronized (LazySingleton.class) {
if (Objects.isNull(singleton)) {
LazySingleton temp = new LazySingleton();
singleton = temp;
}
}
}
return singleton;
}
}
7. 使用靜態(tài)內(nèi)部類實現(xiàn)懶漢式單例
還有一種更加簡單的單例實現(xiàn)方法,利用了 Java 靜態(tài)內(nèi)部類的機制笑跛,當 LazySingleton
被加載時并不會觸發(fā)其靜態(tài)內(nèi)部類 Holder
的加載付魔,只有首次調(diào)用 getSingleton
方法時才會加載靜態(tài)內(nèi)部類 Holder
,又因 Holder 的靜態(tài)屬性 singleton
是 final
飞蹂,所以只會被實例化一次几苍。
package singleton;
public class LazySingleton {
private LazySingleton() {}
private static final class Holder {
private static final LazySingleton singleton = new LazySingleton();
}
public static LazySingleton getSingleton() {
return Holder.singleton;
}
}