單例模式
單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一的對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。
注意:
- 單例類只能有一個(gè)實(shí)例。
- 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
- 單例類必須給所有其他對(duì)象提供這一實(shí)例。
下面是兩種最基礎(chǔ)的單例模式實(shí)現(xiàn)
懶漢式
懶漢式
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這種方式是懶漢式最基本的實(shí)現(xiàn)方式,這種實(shí)現(xiàn)最大的問題就是不支持多線程。因?yàn)闆]有加鎖 synchronized,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
餓漢式
餓漢式
public class Singleton implements Serializable {
private static final Singleton SINGLETON = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return SINGLETON;
}
}
餓漢式基于 classloader 機(jī)制避免了多線程的同步問題,不過,instance 在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance() 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化 instance 顯然沒有達(dá)到 lazy loading 的效果。
綜上所述,我們可以看出使用懶漢式的方式創(chuàng)建的單例模式在多線程下并不是安全的,所以提出了使用雙重校驗(yàn)鎖的方式來實(shí)現(xiàn)懶漢式的單例
雙重校驗(yàn)鎖實(shí)現(xiàn)懶漢式
雙重校驗(yàn)鎖實(shí)現(xiàn)懶漢式
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
// 加了類鎖,保證了線程安全
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
這種方式采用雙鎖機(jī)制,安全且在多線程情況下能保持高性能。
但是我們想想,這樣做完真的就可以保證程序中有且僅有一個(gè)單例對(duì)象的存在了嗎?答案是否定的,我們可以通過反射的方式來破壞單例
使用反射破壞單例
使用反射破壞單例
// 使用反射來破壞單例模式
public static void main(String[] args) throws Exception {
// 獲取普通單例對(duì)象
Singleton instance = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
// 破解 private
constructor.setAccessible(true);
// 執(zhí)行構(gòu)造方法創(chuàng)建對(duì)象
Singleton singleton = constructor.newInstance();
System.out.println(singleton); // Singleton@4554617c
System.out.println(instance); // Singleton@74a14482
System.out.println(singleton == instance); // false
}
我們使用反射獲取了 Singleton 的無參構(gòu)造器,通過調(diào)用 setAccessible(true) 忽視 private 權(quán)限控制符,成功的創(chuàng)建了 Singleton 的對(duì)象。反射創(chuàng)建對(duì)象的根本方法就是獲取了 Singleton 的私有構(gòu)造器,那么針對(duì)的方法也就很簡(jiǎn)單了,只需要我們?cè)谒接袠?gòu)造器中再次判斷單例對(duì)象是否已經(jīng)創(chuàng)建,若存在已創(chuàng)建的單例對(duì)象,則拋出運(yùn)行時(shí)異常即可!
解決使用反射破壞單例
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
// 防止反射調(diào)用無參構(gòu)造器破壞單例模式
if (singleton != null) {
throw new RuntimeException("不能重復(fù)創(chuàng)建該對(duì)象");
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用反射再次創(chuàng)建對(duì)象后運(yùn)行截圖如下:

我們現(xiàn)在已經(jīng)防止了多線程下和使用反射來破壞單例了。現(xiàn)在我們的單例模式是不是真的就安全了呢?其實(shí)除了反射以外,使用 序列化與反序列化也同樣可以破壞單例!
使用序列化反序列化破壞單例
使用序列化反序列化破壞單例
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("tempfile"));
os.writeObject(instance);
ObjectInputStream is = new ObjectInputStream(new FileInputStream("tempfile"));
Singleton singleton = (Singleton) is.readObject();
System.out.println(singleton); // Singleton@4dd8dc3
System.out.println(instance); // Singleton@7f31245a
System.out.println(singleton == instance); // false
}
可以看出單例模式又再次被破壞了。為了解決序列化和反序列化破壞單例模式的問題,我們可以定義一種稱為 readResolve() 的特殊序列化方法。如果我們定義了 readResolve() 方法,那么在對(duì)象被序列化后就會(huì)被調(diào)用。
修改 Singleton 類并增加
addResolve()方法
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
// 防止反射調(diào)用無參構(gòu)造器破壞單例模式
if (singleton != null) {
throw new RuntimeException("不能重復(fù)創(chuàng)建該對(duì)象");
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
/**
* 防止被序列化破壞單例模式
* @return
*/
private Object readResolve() {
return singleton;
}
}
再次運(yùn)行序列化和反序列化程序,可以觀察到如下的結(jié)果

不過我們還有一種最簡(jiǎn)單的方式去創(chuàng)建一個(gè)單例模式,那就是使用 枚舉 來創(chuàng)建單例模式
使用枚舉創(chuàng)建單例模式
enum SingletonEnum {
/**
* 實(shí)例
*/
INSTANCE;
private Singleton singleton = null;
SingletonEnum() {
singleton = new Singleton();
}
public Singleton getInstance() {
return singleton;
}
}
運(yùn)行類 及 結(jié)果
public class Singleton {
public static void main(String[] args) {
Singleton instance = SingletonEnum.INSTANCE.getInstance();
Singleton singleton = SingletonEnum.INSTANCE.getInstance();
System.out.println(instance == singleton); //true
}
}
使用靜態(tài)內(nèi)部類的方式創(chuàng)建單例
通過靜態(tài)內(nèi)部類的方法創(chuàng)建單例同樣實(shí)現(xiàn)了懶加載和線程安全,因?yàn)?Java 在加載一個(gè)類時(shí),其內(nèi)部類不會(huì)同時(shí)被加載。一個(gè)類被加載,當(dāng)且僅當(dāng)其某個(gè)靜態(tài)成員(靜態(tài)域、構(gòu)造器、靜態(tài)方法等)被調(diào)用時(shí)發(fā)生。
public class Singleton implements Serializable {
private Singleton() { }
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
}