在面試中相信不少人會被問到:說說你最瞭解的三個設計模式,平常開發中使用過哪些設計模式等等。最近幾篇文章就來學習一下設計模式,這是第一篇文章,也是最多見的模式——單例模式。java
單例模式(Singleton Pattern
),顧名思義,即保證一個類僅有一個實例,並在全局中提供一個訪問點。git
在實現單例時,要保證一個類僅有一個實例,就不能提供公有的構造方法,任由其餘類建立實例,對應變量也須要爲 static
,只在加載時初始化一次。另外呢,要在全局中都能訪問到,還須要提供一個靜態的公有方法來進行訪問。github
具體實現方式比較多,對於不一樣的場景,也應該選擇不一樣的方式,例如是否須要保證線程安全,是否須要延遲加載。下面具體來看一下。面試
根據上面對單例模式實現的說明,能夠很容易地想到以下實現:設計模式
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() {
return instance;
}
}
複製代碼
這種方式在該類第一次被加載時,就會建立好該實例。這就是所謂的餓漢式,也就是,在想要使用實例時,馬上就能拿到,而不須要進行等待。安全
另外這種方式,由 JVM
保證其線程安全。可是這種方式可能會形成資源消耗,由於有可能這個實例根本就用不到,而進行沒必要要的加載。多線程
上述方式在類加載時就進行實例化,可能會形成沒必要要的加載。那麼咱們能夠在其真正被訪問的時候,再進行實例化,因而能夠寫出以下方式:ide
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() { }
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
複製代碼
在 getInstance
方法中,第一次訪問時,因爲沒有初始化,纔去進行進行初始化,在後續訪問時,直接返回該實例便可。這就是所謂的懶漢式,也就是,它不會提早把實例建立出來,而是將其延遲到第一次被訪問的時候。性能
可是懶漢式存在線程安全問題,以下圖:學習
在多線程場景下,若是有兩個線程同時進入 if
語句中,則這兩個線程分別建立了一個對象,在兩個線程從 if
中退出時,就建立了兩個不同的對象。
既然普通的懶漢式會出現線程安全問題,那麼給建立對象的方法加鎖便可:
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() { }
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
複製代碼
上述這種作法雖然在多線程場景下也能正常工做,也具有延遲加載。但因爲 synchronized
方法鎖住了整個方法,效率比較低。因而,聰明的小夥伴,能夠很容易想到,使用同步方法塊,來減少加鎖的粒度。
看下面兩種作法,加鎖粒度確實減少了,可是它們卻並不能保證線程安全:
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
複製代碼
因爲指定重排序出現問題,後面介紹雙重校驗鎖時會詳細說。
if (instance == null) {
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
複製代碼
若是 synchronized
加在 if
語句外面,這和普通的懶漢式作法同樣,沒有區別。若是有兩個線程分別進入 if
語句,雖然也有加鎖操做,可是兩個線程都會執行實例化,也就是會進行兩次實例化。
因而引出了雙重校驗鎖方式,能夠先判斷對象是否實例化,若是沒有再進行加鎖,再加鎖以後,再次判斷是否實例化,若是仍然沒有實例化,才實例化對象。
這種作法的完整代碼以下:
public class Singleton5 {
private static volatile Singleton5 instance;
private Singleton5() { }
public static Singleton5 getInstance() {
// 若是已經實例化,則直接返回,不用加鎖,提高性能
if (instance == null) {
synchronized (Singleton5.class) {
// 再次檢查,保證線程安全
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
複製代碼
能夠看到,在 synchronized
語句先後,有兩個 if
判斷,這就是所謂的雙重校驗鎖。
使用 volatile
其實,若是僅僅是雙重校驗的話,仍然不能保證線程安全問題。這就要分析 instance = new Singleton5();
這段代碼。
雖然代碼只有一句,但在 JVM
中它其實被分爲三步執行:
instance
分配內存空間;instance
進行初始化;instance
指向分配的內存地址;但因爲編譯器或處理器可能會對指令重排序,執行的順序就有可能變成 1->3->2
。這在單線程環境下不會出現問題,可是在多線程環境下可能會致使一個線程得到尚未初始化的實例。
例如,線程 A
執行了第 1
、3
步後,此時線程 B
調用 getInstance()
方法,判斷 instance
不爲空,所以返回 instance
。但此時 instance
還未被初始化。
因此,就須要使用 volatile
關鍵字來修飾 instance
,禁止編譯器的指令重排序,保證在多線程環境下也能正常運行。
目前雙重校驗鎖的作法看起來不錯,使用延遲加載,在保證線程安全的同時,加鎖粒度也比較小,效率還不錯。那還有沒有其餘方法呢?
那就是使用靜態內部類來實現,來看一下它的實現:
public class Singleton6 {
private Singleton6() { }
private static class InnerSingleton {
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return InnerSingleton.INSTANCE;
}
}
複製代碼
在這種實現中,當外部類 Singleton6
類被加載時,靜態內部類 InnerSingleton
並無被加載。
而是隻有當調用 getInstance
方法,從而訪問類的靜態變量時,纔會加載內部類,從而實例化 INSTANCE
。而且 JVM
能確保 INSTANCE
只能被實例化一次,即它也是線程安全的。
另外,使用枚舉實現單例也是一種不錯的方式,代碼很是簡單:
public enum Singleton6 {
INSTANCE();
Singleton6() { }
}
複製代碼
枚舉的實現中,類被定義爲 final
,其枚舉值被定義爲 static
final
,對枚舉值的初始化放在靜態語句塊中。因此,對象在該類第一次被加載時實例化,這不只避免了線程安全問題,並且也避免了下面提到的反序列化對單例的破壞。
如今來看一下,對象在序列化和反序列化時,是否還可以保證單例。
這裏使用雙重校驗鎖實現的單例類,對 Singleton5
類添加 Serializable
接口,而後進行測試:
public class SingletonTest {
public static void main(String[] args) {
Singleton5 instance1 = Singleton5.getInstance();
try ( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")) ){
oos.writeObject(instance1);
} catch (IOException e) {
e.printStackTrace();
}
Singleton5 instance2 = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))) ){
instance2 = (Singleton5) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(instance1 == instance2);
}
}
// false
複製代碼
能夠看到,對 Singleton5
進行反序列獲得的是一個新的對象,如此就破壞了 Singleton5
的單例性。
咱們能夠在 Singleton5
類中添加一個 readResolve()
方法,並在該方法中指定要返回的對象的生成策略:
public class Singleton5 implements Serializable {
private static volatile Singleton5 instance;
private Singleton5() { }
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
// 添加 readResolve 方法
private Object readResolve() {
return instance;
}
}
複製代碼
經過 debug
方法查看源碼,在 readObject
方法的調用棧中,能夠看到 ObejctStreamClass
類的 invokeReadResolve
方法:
若是定義了 readResolve
方法,會經過反射進行調用,根據指定的策略來生成對象。
JDK#Runtime
該類用於獲取應用運行時的環境。能夠看到這是一個餓漢式的單例。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
複製代碼
Spring#Singleton
在 Spring
中定義 Bean
時,能夠指定是單例仍是多例(默認爲單例):
@Scope("singleton")
複製代碼
查看其源碼,單例模式實現以下:
public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private T singletonInstance;
@Override
public void afterPropertiesSet() throws Exception {
// 掃描配置時,單例模式
// 就會將 initialized 置爲 true
if (isSingleton()) {
this.initialized = true;
// 調用子類方法建立對象
this.singletonInstance = createInstance();
this.earlySingletonInstance = null;
}
}
@Override
public final T getObject() throws Exception {
if (isSingleton()) {
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
}
else {
return createInstance();
}
}
}
複製代碼