前言
單例模式 (Singleton) 是一種創建型模式,指某個類採用Singleton模式,則在這個類被創建後,只可能產生一個實例供外部訪問,並且提供一個全局的訪問點。
正文
(一). 優缺點
Java中單例模式 (Singleton) 是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程序中,某個類只有一個實例存在。一些管理器和控制器常被設計成單例模式。
1. 優點
- 提供了對唯一實例的受控訪問。
- 由於在系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬的對象單例模式無疑可以提高系統的性能。
- 可以根據實際情況需要,在單例模式的基礎上擴展做出雙例模式,多例模式。
2. 缺點
- 單例類的職責過重,裡面的代碼可能會過於複雜,在一定程度上違背了“ 單一職責原則”。
- 如果實例化的對象長時間不被利用,會被系統認為是垃圾而被回收,這將導致對象狀態的丟失。
(二). 具體實現
簡單點說,就是一個應用程序中,某個類的實例對象只有一個,你沒有辦法去new,因為構造器是被private修飾的,一般通過getInstance()的方法來獲取它們的實例。getInstance()的返回值是一個同一個對象的引用,並不是一個新的實例。單例模式 實現起來也很容易,以下給出六種實現方式:
1. 餓漢式
特點:線程安全,無法實現實例懶加載策略。
public class Singleton1 {
private final static Singleton1 singleton1 = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return singleton1;
}
}
2. 懶漢式
特點:線程不安全,實現了實例懶加載策略。
public class Singleton2 {
private final static Singleton2 singleton2;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (singleton2 == null)
singleton2 = new Singleton2();
return singleton2;
}
}
3. 全局鎖式
特點:線程安全,且實現了懶加載策略,但是線程同步時效率不高。
public class Singleton3 {
private final static Singleton3 singleton3;
private Singleton3() {
}
public synchronized static Singleton3 getInstance() {
if (singleton3 == null)
singleton3 = new Singleton3();
return singleton3;
}
}
4. 靜態代碼塊式
特點:線程安全,類主動加載時才初始化實例,實現了懶加載策略,且線程安全。
public class Singleton4 {
private final static Singleton4 singleton4;
private Singleton4() {
}
static {
singleton4 = new Singleton4();
}
public static Singleton4 getInstance() {
return singleton4;
}
}
5. 雙重校驗鎖式
特點:線程安全,且實現了懶加載策略,同時保證了線程同步時的效率。但是volatile強制當前線程每次讀操作進行時,保證所有其他的線程的寫操作已完成。volatile使得JVM內部的編譯器捨棄了編譯時優化,對於性能有一定的影響。
public class Singleton5 {
private static volatile Singleton5 singleton5;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (singleton5 == null) {
synchronized (Singleton5.class) {
if (singleton5 == null) {
singleton5 = new Singleton5();
}
}
}
return singleton5;
}
}
6. 靜態內部類式【推薦】
特點:線程安全,不存在線程同步問題,且單例對象在程序第一次 getInstance() 時主動加載 SingletonHolder 和其 靜態成員 INSTANCE,因而實現了懶加載策略。
public class Singleton6 {
private Singleton6() {
}
private static class SingletonHolder {
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return Singleton6.SingletonHolder.INSTANCE;
}
}
7. 枚舉方式【作者推薦】
特點:線程安全,不存在線程同步問題,且單例對象在枚舉類型 INSTANCE 第一次引用時通過枚舉的 構造函數 初始化,因而實現了懶加載策略。
public class Singleton7 {
private Singleton7() {
}
enum SingletonEnum {
INSTANCE;
private final Singleton7 singleton7;
private SingletonEnum() {
singleton7 = new Singleton7();
}
}
public static Singleton7 getInstance() {
return SingletonEnum.INSTANCE.singleton7;
}
public static void main(String[] args) {
IntStream.rangeClosed(0, 100).forEach(i -> new Thread() {
public void run() {
out.println(Singleton7.getInstance());
};
}.start());
}
}
這種方式是Effective Java作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊。不過,由於JDK 1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏。
測試代碼如下:
@FixMethodOrder
public class SingletonTester {
protected final static int FROM = 0;
protected final static int TO = 1000;
protected static HashSet<object> GLOBAL_SET = new HashSet<>();/<object>
static {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
out.println();
// count
GLOBAL_SET.forEach((value) -> {
out.println("Global [" + value + "]");
});
}
});
}
// testSingleton1
@Test
public void testSingleton1() throws Exception {
final HashSet<object> localSet = new HashSet<>();/<object>
final CountDownLatch latch = new CountDownLatch(TO);
IntStream.range(FROM, TO).forEach(i -> new Thread() {
public void run() {
Singleton1 singleton = Singleton1.getInstance();
count(singleton);
}
protected void count(Singleton1 singleton) {
localSet.add(singleton);
out.println("Size of HashSet1 is: [" + localSet.size() + "]");
// 計數減1,釋放線程
latch.countDown();
};
}.start());
// 等待子線程執行結束
latch.await();
synchronized (localSet) {
// count
localSet.forEach((value) -> {
out.println("[" + value + "]");
out.println();
});
GLOBAL_SET.addAll(localSet);
}
}
// testSingleton2
// testSingleton3
// testSingleton4
// testSingleton5
// testSingleton6
// testSingleton7
}
測試結果截圖如下,測試用例反映7種單例模式的方案都可以正常執行:
這裡只演示其中一種單例方式,運行截圖如下:
上圖顯示,通過 getInstance() 得到的實例全局唯一。對於其餘六中方式,根據測試用例測試得到的結果一致,大家可以自行測試。
總結
本文總結了七種Java中實現單例模式的方法,其中使用雙重校驗鎖、靜態內部類 和 枚舉類 的方式可以解決大部分問題。其中,極為推薦 靜態內部類 和 枚舉類 這兩種實現方式。
閱讀更多 程序猿的內心獨白 的文章