單例模式 (Singleton) (重點)
一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類
1) 為什么要使用單例
1.表示全局唯一
如果有些數(shù)據(jù)在系統(tǒng)中應該且只能保存一份,那就應該設計為單例類:
- 配置類:在系統(tǒng)中,我們只有一個配置文件,當配置文件被加載到內(nèi)存之后,應該被映射為一個唯一的【配置實例】
- 全局計數(shù)器:我們使用一個全局的計數(shù)器進行數(shù)據(jù)統(tǒng)計、生成全局遞增ID等功能。若計數(shù)器不唯一,很有可能產(chǎn)生統(tǒng)計無效,ID重復等
2.處理資源訪問沖突
如果使用單個實例輸出日志,鎖【this】即可。
如果要保證JVM級別防止日志文件訪問沖突,鎖【class】即可。
如果要保證集群服務級別的防止日志文件訪問沖突,加分布式鎖即可
2) 如何實現(xiàn)一個單例
常見的單例設計模式,有如下五種寫法,在編寫單例代碼的時候要注意以下幾點:
- 1.構造器需要私有化
- 2.暴露一個公共的獲取單例對象的接口
- 3.是否支持懶加載(延遲加載)
- 4.是否線程安全
2.a) 餓漢式
在類加載的時候,instance 靜態(tài)實例就已經(jīng)創(chuàng)建并初始化好了,所以,instance 實例的創(chuàng)建過程是線程安全的
/**
* 餓漢式單例的實現(xiàn)
* - 不支持懶加載
* - jvm保證線程安全
*/
public class EagerSingleton {
/**
* 當啟動程序的時候,就創(chuàng)建這個實例
*/
// 1.持有一個jvm全局唯一的實例
private static final EagerSingleton instance = new EagerSingleton();
// 2.為了避免別人隨意的創(chuàng)建,需要私有化構造器
private EagerSingleton() {
}
// 3.暴露一個方法,用來獲取實例
public static EagerSingleton getInstance() {
return instance;
}
}
2.b) 懶漢式
懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載,具體的代碼實現(xiàn)如下所示:
支持延遲加載
/**
* 懶漢式單例的實現(xiàn)
* - 支持懶加載
*/
public class LazySingleton {
/**
* 當需要使用這個實例的時候,再創(chuàng)建這個實例
*/
// 1.持有一個jvm全局唯一的實例
private static LazySingleton instance;
// 2.為了避免別人隨意的創(chuàng)建,需要私有化構造器
private LazySingleton() {
}
// 3.暴露一個方法,用來獲取實例
// - 懶加載-線程不安全,因為當面對大量并發(fā)請求時,有可能會有超過一個線程同時執(zhí)行此方法,是無法保證其單例的特點
// - 加鎖:使用 synchronized,(對.class加鎖) 但是方法上加鎖會極大的降低獲取單例對象的并發(fā)度
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2.c) 雙重檢查鎖
餓漢式不支持延遲加載,懶漢式有性能問題,不支持高并發(fā)。既支持延遲加載、又支持高并發(fā)的單例實現(xiàn)方式,也就是雙重檢測鎖:
/**
* 雙重檢查鎖單例的實現(xiàn)
*/
public class DoubleCheckLockSingleton {
// 1.持有一個jvm全局唯一的實例
// - 因為創(chuàng)建對象不是一個原子性操作,即使使用雙重檢查鎖,也可能在創(chuàng)建過程中產(chǎn)生半初始化狀態(tài)
// - volatile 1.保證內(nèi)存可見 2.保存有序性
// - jdk1.9以上,不加volatile也可以,jvm內(nèi)部處理有序性
private static volatile DoubleCheckLockSingleton instance;
// 2.為了避免別人隨意的創(chuàng)建,需要私有化構造器
private DoubleCheckLockSingleton() {
}
// 3.暴露一個方法,用來獲取實例
// - 第一次創(chuàng)建需要上鎖,一旦創(chuàng)建完成,就不再需要上鎖
// - 事實上獲取單例并沒有線程安全的問題
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
// 創(chuàng)建
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
2.d) 靜態(tài)內(nèi)部類
比雙重檢測更加簡單的實現(xiàn)方法,那就是利用 Java 的靜態(tài)內(nèi)部類。它有點類似餓漢式,但又能做到了延遲加載。
當外部類 InnerSingleton()被加載的時候,并不會創(chuàng)建 InnerSingleton的實例對象。只有當調(diào)用 getInstance() 方法時,InnerSingletonHolder 才會被加載,這個時候才會創(chuàng)建 instance實例。
/**
* 靜態(tài)內(nèi)部類的方式實現(xiàn)單例
*/
public class InnerSingleton {
// 1.私有化構造器
private InnerSingleton() {
}
// 2.提供一個方法,獲取單例對象
public InnerSingleton getInstance() {
return InnerSingletonHolder.instance;
}
// 3.定義內(nèi)部類,來持有實例
// - 特性:類加載的時機 --> 一個類會在第一次使用的時候被加載
// - 實例會在內(nèi)部類加載(調(diào)用getInstance()方法之后)會創(chuàng)建
private static class InnerSingletonHolder {
private static final InnerSingleton instance = new InnerSingleton();
}
}
2.e) 枚舉類
基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。
/**
* 枚舉:累加器
*/
public enum GlobalCounter {
// 這個INSTANCE是一個單例
// 對于枚舉類。任何一個枚舉項就是一個單例
// 本質上就是 static final GlobalCounter instance = new GlobalCounter()
INSTANCE;
private AtomicLong atomicLong = new AtomicLong(0);
public Long getNumber() {
return atomicLong.getAndIncrement();
}
}
2.f) 反射入侵
事實上,我們想要阻止其他人構造實例僅僅私有化構造器還是不夠的,因為我們還可以使用反射獲取私有構造器進行構造,當然使用枚舉的方式是可以解決這個問題的,對于其他的書寫方案,我們通過下邊的方式解決:
// 反射代碼
Class<DoubleCheckLockSingleton> instance = DoubleCheckLockSingleton.class;
Constructor<DoubleCheckLockSingleton> constructor = instance.getDeclaredConstructor();
constructor.setAccessible(true);
boolean flag = DoubleCheckLockSingleton.getInstance() == constructor.newInstance();
log.info("flag -> {}",flag);
/**
* 單例的防止反射入侵的代碼實現(xiàn)
*/
public class ReflectSingleton {
/**
* 可以使用反射獲取私有構造器進行構造
*/
private static volatile ReflectSingleton instance;
// 為了避免別人隨意的創(chuàng)建,需要私有化構造器
private ReflectSingleton() {
// 升級版本 --> 不要讓人使用反射創(chuàng)建
if (instance != null) {
throw new RuntimeException("該對象是單例,無法創(chuàng)建多個");
}
}
public static ReflectSingleton getInstance() {
if (instance == null) {
synchronized (ReflectSingleton.class) {
// 創(chuàng)建
if (instance == null) {
instance = new ReflectSingleton();
}
}
}
return instance;
}
}
2.g) 序列化與反序列化安全
事實上,到目前為止,我們的單例依然是有漏洞的
/**
* 通過序列化
*/
@Test
public void testSerialize() throws Exception {
// 獲取單例并序列化
SerializableSingleton instance = SerializableSingleton.getInstance();
FileOutputStream fout = new FileOutputStream("F://singleton.txt");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(instance);
// 將實例反序列化出來
FileInputStream fin = new FileInputStream("F://singleton.txt");
ObjectInputStream in = new ObjectInputStream(fin);
Object o = in.readObject();
log.info("是同一個實例嗎 {}", o == instance); // 是同一個實例嗎 false
}
在進行反序列化時,會嘗試執(zhí)行readResolve方法,并將返回值作為反序列化的結果,而不會克隆一個新的實例,保證jvm中僅僅有一個實例存在
public class Singleton implements Serializable {
// 省略其他的內(nèi)容
public static Singleton getInstance() {
}
// 需要加這么一個方法
public Object readResolve(){
return singleton;
}
}
3) 單例存在的問題
在項目中使用單例,都是用它來表示一些全局唯一類,比如配置信息類、連接池類、ID 生成器類。單例模式書寫簡潔、使用方便,在代碼中,我們不需要創(chuàng)建對象。但是,這種使用方法有點類似硬編碼(hard code),會帶來諸多問題,所以我們一般會使用spring的單例容器作為替代方案。文章來源:http://www.zghlxwxcb.cn/news/detail-690500.html
3.a) 無法支持面向對象編程
OOP 的三大特性是封裝、繼承、多態(tài)。單例將構造私有化,直接導致的結果就是,他無法成為其他類的父類,這就相當于直接放棄了繼承和多態(tài)的特性,也就相當于損失了可以應對未來需求變化的擴展性,以后一旦有擴展需求,比如寫一個類似的具有絕大部分相同功能的單例,我們不得不新建一個十分【雷同】的單例。文章來源地址http://www.zghlxwxcb.cn/news/detail-690500.html
到了這里,關于設計模式-單例模式Singleton的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!