前言
單例模式相對來說,設計比較簡單,但是實現方式多種多樣,我們需要從線程安全、高性能、懶加載方面進行評估。
餓漢式
實例代碼如下:
public final class Singleton1 {
private byte[] data = new byte[1024];
private static Singleton1 instance = new Singleton1();
private Singleton1() {
System.out.println("Singleton1 實例化");
}
public static Singleton1 getInstance() {
return instance;
}
}
根據前面可以知道,在類的初始化階段類變量進行初始化,就是instance變量會被初始化,同時呢1k的空間data也被創建,我們可以寫段代碼測試下:
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> aClass1 = Class.forName("fast.cloud.nacos.juc.singleton.Singleton1");
}
}
運行代碼會輸出:
Singleton1 實例化
這也說明了Class.forName
方法會對類進行初始化
關於這點不懂得可以看看前面的博客,有解釋到的,Class.forName 和 ClassLoader的區別
如果一個類屬性較少,佔用內存也較小,用餓漢式也未嘗不可。
總而言之,餓漢式不支持懶加載,可以保證多線程下只被加載一次,性能也比較高。
懶漢式
所謂懶漢式就是當你使用的時候再去創建,代碼如下
public final class Singleton2 {
private byte[] data = new byte[1024];
private static Singleton2 instance = null;
private Singleton2() {
System.out.println("Singleton2 實例化");
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
這種方式會有一個問題,當兩個線程都走在 instance == null
時,這個時候會創建兩個實例,線程不安全。
懶漢式 + 同步方法
代碼如下
public final class Singleton3 {
private byte[] data = new byte[1024];
private static Singleton3 instance = null;
private Singleton3() {
System.out.println("Singleton3 實例化");
}
//加鎖,每次只有一個線程獲得
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
這種方式的問題在於,synchronized
的排他性,同一時刻,只能被一個線程訪問,性能低下。
Double-Check
這種方法就是,在初次初始化的時候進行加鎖,之後就需要加鎖了,提高了效率。
public final class Singleton4 {
private byte[] data = new byte[1024];
private static Singleton4 instance = null;
private Singleton4() {
System.out.println("Singleton4 實例化");
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
這種會出現空指針異常,我們來逐步分析下:
主要在於singleton = new Singleton4()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null了)
在JVM的即時編譯器中存在指令重排序的優化。
也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,
被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
解決方案就是加上volatile
,就可以了
public final class Singleton5 {
private byte[] data = new byte[1024];
/**
* static 和 volatile 切換,編譯器不會報錯
*/
private static volatile Singleton5 instance = null;
private Singleton5() {
System.out.println("Singleton5 實例化");
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
這了需要注意
volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,
不會調用讀操作(if (instance == null))。
Holder方式
Holder方式借住類加載的特點,同一類加載器只會對同一個類加載一次(這個時候可能被問到類加載器底層如何保證只被加載一次,可以翻翻我前面的博客)
public final class Singleton6 {
private byte[] data = new byte[1024];
private static final class Holder {
private static Singleton6 instance = new Singleton6();
}
public static Singleton6 getInstance() {
return Holder.instance;
}
}
Holder
會在編譯的時候被收集到<clinit>
方法中,該方法可以保證線程安全。
Singleton6
也可以保證懶加載,只有在初次調用getInstance
方法時,纔會初始化Holder
中的實例
枚舉方式
利用自動序列化機制,保證了線程的絕對安全
public enum Singleton7 {
INSTANCE;
Singleton7() {
System.out.println("Singleton7 實例化");
}
public static void method() {
//調用該方法,會導致初始化
}
public static Singleton7 getInstance() {
return INSTANCE;
}
}
這種方法如果調用了外部調用了靜態方法也會初始化,可以加上我們的Holder
模式
枚舉-Holder
public class Singleton8 {
private byte[] data = new byte[1024];
private Singleton8() {
}
public static void method() {
//調用該方法,不會導致初始化
}
private enum EnumHolder{
INSTANCE;
private Singleton8 instance;
private EnumHolder() {
System.out.println("Singleton8 實例化");
this.instance = new Singleton8();
}
public Singleton8 getInstance() {
return instance;
}
}
public Singleton8 getInstance() {
return EnumHolder.INSTANCE.getInstance();
}
}
總結
開發過程中,個人用的比較多的也是Holder
模式和枚舉模式吧。