目錄
分類
- 懶漢式:懶漢式是指應用啓動時並不會初始化相應的實例,而是在第一次使用時加載,也就是所謂的延時加載吧,關於延時加載還有很多話聊,筆者就不一一談了。
- 餓漢式:餓漢式是指應用啓動時就初始化相應的實例,可能說相對來說比較簡單。
餓漢式
先講講餓漢式,這個比較簡單,直接加載就可以了。直接上代碼:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
也有人這樣寫,不過原理是一樣的,都是在類靜態初始化階段初始化實例:
public class Singleton {
private static Singleton singleton;
private Singleton(){}
static {
singleton = new Singleton();
}
public static Singleton getInstance(){
return singleton;
}
}
餓漢式沒過多可講的,下面我們分析一下懶漢式。
懶漢式
最簡單的實現
不多說,直接上代碼。
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這個代碼在單線程環境下會良好運行,但在多線程環境下會有較大問題,也就是所謂的線程不安全。設想一下,線程A在運行到singleton = new Singleton()
時,線程B剛好在進行singleton == null
, 這時線程B會繼續進入if
塊,而重新對線程A已經實例化的singleton
進行重新實例化,這樣就衝突了,這還是簡單的兩個線程,如果是多個線程同時進行,那就比較嚴重了。
解決這個問題的最簡單方法是用同步塊synchronized
Synchronized
實現
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這個肯定是線程安全的,因爲整個方法都被鎖住了,但這樣解決了初始化實例的問題,卻導致了每次只能有一個線程調用該方法,其他線程都會被鎖住,這樣就會導致較大的性能損失。解決這個問題可以使用DCL(Double Check Lock)
DCL非線程安全的實現
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if (singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
我們先分析一下這個代碼。
- 只有實例第一次被訪問時,纔會有線程進入同步塊,這樣極大提高了性能。避免了
synchronized
帶來的較大性能損失。 - 第一次訪問時,如果有多個線程同時進入
if
塊,只有第一個線程會獲得鎖,其他線程被阻塞,第一個線程可以創建實例。 - 第一次訪問時,被阻塞的線程會進入同步塊,進行第二次check,如果此時實例不爲
null
,則返回。
仔細一想,這個代碼挺完美的,但是不是這個樣子的,具體問題出現在哪呢?
Java程序創建一個實例的過程爲:
- 分配內存空間
- 初始化對象
- 將內存空間的地址賦值給對應的引用
但是由於指令重排的原因,什麼是指令重排?指令重排序是JVM爲了優化指令,提高程序運行效率。指令重排序包括編譯器重排序和運行時重排序。JVM規範規定,指令重排序可以在不影響單線程程序執行結果前提下進行。既然這樣,那麼在應用真正運行時可能是這個樣子的: - 分配內存空間
- 將內存空間的地址賦值給對應的引用
- 初始化對象
線程執行順序
根據上圖分析可以看出new Singleton()
時可能會導致錯誤。所以解決這個問題的方法:
- 禁止初始化階段的發生重排序
- 初始化階段可以發生重排序,但不能被其他線程“知道”
DCL線程安全實現--volatile實現
volatile
是Java中的一個關鍵字,使用該關鍵字修飾的變量在被變更時會被其他變量可見。
public class Singleton {
//通過volatile關鍵字來確保安全
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;
}
}
基於ClassLoader的實現
這個方案是利用ClassLoader本身的機制來避免多個線程同時實例化該變量。也就是解決的上面說的2. 初始化階段可以發生重排序,但不能被其他線程“知道”。
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
基於枚舉的實現
public enum DataSourceEnum {
DATASOURCE;
private DBConnection connection = null;
private DataSourceEnum() {
connection = new DBConnection();
}
public DBConnection getConnection() {
return connection;
}
}
其實Enum就是一個普通的類,它繼承自java.lang.Enum類
把上面枚舉編譯後的字節碼反編譯,得到的代碼如下:
public final class DataSourceEnum extends Enum<DataSourceEnum> {
public static final DataSourceEnum DATASOURCE;
public static DataSourceEnum[] values();
public static DataSourceEnum valueOf(String s);
static {};
}
由反編譯後的代碼可知,DATASOURCE 被聲明爲 static 的,根據在【單例深思】餓漢式與類加載 中所描述的類加載過程,可以知道虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖、同步。所以,枚舉實現是在實例化時是線程安全。