深入理解單例設(shè)計(jì)模式
作者:惜鳥(niǎo)
來(lái)源:Segmentfault 思否
一、概述
單例模式是什么 單例模式的使用場(chǎng)景 單例模式的優(yōu)缺點(diǎn) 單例模式的實(shí)現(xiàn)(重點(diǎn)) 總結(jié)
二、單例模式是什么
三、單例模式的使用場(chǎng)景
需要生成唯一序列的環(huán)境 需要頻繁實(shí)例化然后銷毀的對(duì)象。 創(chuàng)建對(duì)象時(shí)耗時(shí)過(guò)多或者耗資源過(guò)多,但又經(jīng)常用到的對(duì)象。 方便資源相互通信的環(huán)境
四、單例模式的優(yōu)缺點(diǎn)
在內(nèi)存中只有一個(gè)對(duì)象,節(jié)省內(nèi)存空間; 避免頻繁的創(chuàng)建銷毀對(duì)象,減輕 GC 工作,同時(shí)可以提高性能; 避免對(duì)共享資源的多重占用,簡(jiǎn)化訪問(wèn); 為整個(gè)系統(tǒng)提供一個(gè)全局訪問(wèn)點(diǎn)。
不適用于變化頻繁的對(duì)象; 濫用單例將帶來(lái)一些負(fù)面問(wèn)題,如為了節(jié)省資源將數(shù)據(jù)庫(kù)連接池對(duì)象設(shè)計(jì)為的單例類,可能會(huì)導(dǎo)致共享連接池對(duì)象的程序過(guò)多而出現(xiàn)連接池溢出; 如果實(shí)例化的對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為該對(duì)象是垃圾而被回收,這可能會(huì)導(dǎo)致對(duì)象狀態(tài)的丟失;
五、單例模式的實(shí)現(xiàn)(重點(diǎn))
私有化構(gòu)造方法,避免外部類通過(guò) new 創(chuàng)建對(duì)象 定義一個(gè)私有的靜態(tài)變量持有自己的類型 對(duì)外提供一個(gè)靜態(tài)的公共方法來(lái)獲取實(shí)例 如果實(shí)現(xiàn)了序列化接口需要保證反序列化不會(huì)重新創(chuàng)建對(duì)象
1、餓漢式,線程安全
缺點(diǎn):不是懶加載,類加載時(shí)就初始化,浪費(fèi)內(nèi)存空間
/**
* 餓漢式單例測(cè)試
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化構(gòu)造方法
private Singleton(){}
// 2、定義一個(gè)靜態(tài)變量指向自己類型
private final static Singleton instance = new Singleton();
// 3、對(duì)外提供一個(gè)公共的方法獲取實(shí)例
public static Singleton getInstance() {
return instance;
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 使用反射破壞單例
// 獲取空參構(gòu)造方法
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 設(shè)置強(qiáng)制訪問(wèn)
declaredConstructor.setAccessible(true);
// 創(chuàng)建實(shí)例
Singleton singleton = declaredConstructor.newInstance();
System.out.println("反射創(chuàng)建的實(shí)例" + singleton);
System.out.println("正常創(chuàng)建的實(shí)例" + Singleton.getInstance());
System.out.println("正常創(chuàng)建的實(shí)例" + Singleton.getInstance());
}
}
反射創(chuàng)建的實(shí)例com.example.spring.demo.single.Singleton@6267c3bb
正常創(chuàng)建的實(shí)例com.example.spring.demo.single.Singleton@533ddba
正常創(chuàng)建的實(shí)例com.example.spring.demo.single.Singleton@533ddba
2、懶漢式,線程不安全
缺點(diǎn):線程不安全
/**
* 懶漢式單例,線程不安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化構(gòu)造方法
private Singleton(){ }
// 2、定義一個(gè)靜態(tài)變量指向自己類型
private static Singleton instance;
// 3、對(duì)外提供一個(gè)公共的方法獲取實(shí)例
public static Singleton getInstance() {
// 判斷為 null 的時(shí)候再創(chuàng)建對(duì)象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("多線程創(chuàng)建的單例:" + Singleton.getInstance());
}).start();
}
}
}
多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@18396bd5
多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@7f23db98
多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@5000d44
3、懶漢式,線程安全
synchronized 關(guān)鍵字加鎖保證線程安全,synchronized 可以添加在方法上面,也可以添加在代碼塊上面,這里演示添加在方法上面,存在的問(wèn)題是每一次調(diào)用 getInstance 獲取實(shí)例時(shí)都需要加鎖和釋放鎖,這樣是非常影響性能的。缺點(diǎn):效率較低
/**
* 懶漢式單例,方法上面添加 synchronized 保證線程安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化構(gòu)造方法
private Singleton(){ }
// 2、定義一個(gè)靜態(tài)變量指向自己類型
private static Singleton instance;
// 3、對(duì)外提供一個(gè)公共的方法獲取實(shí)例
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4、雙重檢查鎖(DCL, 即 double-checked locking)
/**
* 雙重檢查鎖(DCL, 即 double-checked locking)
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化構(gòu)造方法
private Singleton() {
}
// 2、定義一個(gè)靜態(tài)變量指向自己類型
private volatile static Singleton instance;
// 3、對(duì)外提供一個(gè)公共的方法獲取實(shí)例
public synchronized static Singleton getInstance() {
// 第一重檢查是否為 null
if (instance == null) {
// 使用 synchronized 加鎖
synchronized (Singleton.class) {
// 第二重檢查是否為 null
if (instance == null) {
// new 關(guān)鍵字創(chuàng)建對(duì)象不是原子操作
instance = new Singleton();
}
}
}
return instance;
}
}
缺點(diǎn):實(shí)現(xiàn)較復(fù)雜
volatile 關(guān)鍵字的使用,關(guān)于 volatile 的詳細(xì)介紹可以直接搜索 volatile 關(guān)鍵字即可,有很多寫的非常好的文章,這里不做詳細(xì)介紹,簡(jiǎn)單說(shuō)明一下,雙重檢查鎖中使用 volatile 的兩個(gè)重要特性:可見(jiàn)性、禁止指令重排序volatile?new 關(guān)鍵字創(chuàng)建對(duì)象不是原子操作,創(chuàng)建一個(gè)對(duì)象會(huì)經(jīng)歷下面的步驟:在堆內(nèi)存開(kāi)辟內(nèi)存空間 調(diào)用構(gòu)造方法,初始化對(duì)象 引用變量指向堆內(nèi)存空間


1 2 3 或者 1 3 2 ,因此當(dāng)某個(gè)線程在亂序運(yùn)行 1 3 2 指令的時(shí)候,引用變量指向堆內(nèi)存空間,這個(gè)對(duì)象不為 null,但是沒(méi)有初始化,其他線程有可能這個(gè)時(shí)候進(jìn)入了 getInstance 的第一個(gè) if(instance == null) 判斷不為 nulll ,導(dǎo)致錯(cuò)誤使用了沒(méi)有初始化的非 null 實(shí)例,這樣的話就會(huì)出現(xiàn)異常,這個(gè)就是著名的 DCL 失效問(wèn)題。volatile 關(guān)鍵字以后,會(huì)通過(guò)在創(chuàng)建對(duì)象指令的前后添加內(nèi)存屏障來(lái)禁止指令重排序,就可以避免這個(gè)問(wèn)題,而且對(duì) volatile 修飾的變量的修改對(duì)其他任何線程都是可見(jiàn)的。5、靜態(tài)內(nèi)部類
/**
* 靜態(tài)內(nèi)部類實(shí)現(xiàn)單例
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化構(gòu)造方法
private Singleton() {
}
// 2、對(duì)外提供獲取實(shí)例的公共方法
public static Singleton getInstance() {
return InnerClass.INSTANCE;
}
// 定義靜態(tài)內(nèi)部類
private static class InnerClass{
private final static Singleton INSTANCE = new Singleton();
}
}
遇到 new、getstatic、putstatic、invokestatic這4條字節(jié)碼指令時(shí)。生成這4條指令最常見(jiàn)的 Java 代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段(final修飾除外,被final修飾的靜態(tài)字段是常量,已在編譯期把結(jié)果放入常量池)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。使用 java.lang.reflect包方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候。當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類的初始化。 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。 當(dāng)使用JDK 1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,則需要先觸發(fā)這個(gè)方法句柄所對(duì)應(yīng)的類的初始化。
INSTANCE 在創(chuàng)建過(guò)程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機(jī)》中,有這么一句話:<clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 <clinit>() 方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞(需要注意的是,其他線程雖然會(huì)被阻塞,但如果執(zhí)行<clinit>()方法后,其他線程喚醒之后不會(huì)再次進(jìn)入<clinit>()方法。同一個(gè)加載器下,一個(gè)類型只會(huì)初始化一次。),在實(shí)際應(yīng)用中,這種阻塞往往是很隱蔽的。6、枚舉單例
/**
* 枚舉實(shí)現(xiàn)單例
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public enum Singleton {
INSTANCE;
public void doSomething(String str) {
System.out.println(str);
}
}
Singleton singleton = Singleton.INSTANCE;
javap Singleton.class
Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
public static final com.spring.demo.singleton.Singleton INSTANCE;
public static com.spring.demo.singleton.Singleton[] values();
public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
public void doSomething(java.lang.String);
static {};
}
static final 修飾,所以可以通過(guò)類名直接調(diào)用,并且創(chuàng)建對(duì)象的實(shí)例是在靜態(tài)代碼塊中創(chuàng)建的,因?yàn)?static 類型的屬性會(huì)在類被加載之后被初始化,當(dāng)一個(gè)Java類第一次被真正使用到的時(shí)候靜態(tài)資源被初始化、Java類的加載和初始化過(guò)程都是線程安全的,所以創(chuàng)建一個(gè)enum類型是線程安全的。public class Test {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething("hello enum");
// 嘗試使用反射破壞單例
// 枚舉類沒(méi)有空參構(gòu)造方法,反編譯后可以看到枚舉有一個(gè)兩個(gè)參數(shù)的構(gòu)造方法
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
// 設(shè)置強(qiáng)制訪問(wèn)
declaredConstructor.setAccessible(true);
// 創(chuàng)建實(shí)例,這里會(huì)報(bào)錯(cuò),因?yàn)闊o(wú)法通過(guò)反射創(chuàng)建枚舉的實(shí)例
Singleton enumSingleton = declaredConstructor.newInstance();
System.out.println(enumSingleton);
}
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at com.spring.demo.singleton.Test.main(Test.java:24)
newInstance() 方法,有如下判斷:
六、總結(jié)
public class Singleton implements Serializable {
// 1、私有化構(gòu)造方法
private Singleton() {
}
// 2、對(duì)外提供獲取實(shí)例的公共方法
public static Singleton getInstance() {
return InnerClass.instance;
}
// 定義靜態(tài)內(nèi)部類
private static class InnerClass{
private final static Singleton instance = new Singleton();
}
// 對(duì)象被反序列化之后,這個(gè)方法立即被調(diào)用,我們重寫這個(gè)方法返回單例對(duì)象.
protected Object readResolve() {
return getInstance();
}
}
多線程- 在多線程應(yīng)用程序中必須使用單例時(shí),應(yīng)特別小心。
序列化- 當(dāng)單例實(shí)現(xiàn) Serializable 接口時(shí),他們必須實(shí)現(xiàn) readResolve 方法以避免有 2 個(gè)不同的對(duì)象。
類加載器- 如果 Singleton 類由 2 個(gè)不同的類加載器加載,我們將有 2 個(gè)不同的類,每個(gè)類加載一個(gè)。
由類名表示的全局訪問(wèn)點(diǎn)- 使用類名獲取單例實(shí)例。這是一種訪問(wèn)它的簡(jiǎn)單方法,但它不是很靈活。如果我們需要替換Sigleton類,代碼中的所有引用都應(yīng)該相應(yīng)地改變。

評(píng)論
圖片
表情
