一、單例模式簡介
單(dan)例模式(shi)(Singleton Pattern)是(shi) Java 中最簡單(dan)的設計模式(shi)之一。這種類(lei)型的設計模式(shi)屬于創建型模式(shi),它提(ti)供了一種創建對象的最佳方(fang)式(shi)。
這(zhe)種模式涉及到一個單一的(de)(de)類,該(gai)(gai)類負責(ze)創建自己的(de)(de)對(dui)象(xiang)(xiang),同時確保只有單個對(dui)象(xiang)(xiang)被(bei)創建。這(zhe)個類提供(gong)了(le)一種訪問其唯一的(de)(de)對(dui)象(xiang)(xiang)的(de)(de)方式,可以直接訪問,不(bu)需要實例(li)化(hua)該(gai)(gai)類的(de)(de)對(dui)象(xiang)(xiang)。
- 單例類只能有一個實例。
- 單例類必須自己創建自己的唯一實例。
- 單例類必須給所有其他對象提供這一實例。
二、單例實現方式
單例實現方式主要有四(si)種(zhong):餓(e)漢(han)、懶漢(han)、枚舉類、靜態內(nei)部類。
(1)餓漢式
// 問題1:為什么加 final
// 問題2:如果實現了序列化接口, 還要做什么來防止反序列化破壞單例
public final class Singleton implements Serializable {
// 問題3:為什么設置為私有? 是否能防止反射創建新的實例?
private Singleton() {}
// 問題4:這樣初始化是否能保證單例對象創建時的線程安全?
private static final Singleton INSTANCE = new Singleton();
// 問題5:為什么提供靜態方法而不是直接將 INSTANCE 設置為 public, 說出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
- 問題1:防止單例類存在子類覆蓋原來的單例方法,無法被繼承。
- 問題2:序列化使得能夠通過反序列化來創建對象,而不用通過new方法,通過反序列化得到的對象不是原來的對象,破壞了單例,添加readResolve()方法來解決,反序列化會先調用該方法創建對象。
- 問題3:構造函數只能夠被本身所調用,而不public,但是不能夠防止反射創建新的實例,反射能夠獲取類的加載器,直接創建對象。
- 問題4:可以,static對象在類加載時就初始化,而類加載方法是線程安全的。
- 問題5:static方法可以通過類名直接調用,而非靜態方法需要先獲得對象,矛盾;提供更好的封裝性;能夠重構為懶漢模式。
(2)枚舉
// 問題1:枚舉單例是如何限制實例個數的
// 問題2:枚舉單例在創建時是否有并發問題
// 問題3:枚舉單例能否被反射破壞單例
// 問題4:枚舉單例能否被反序列化破壞單例
// 問題5:枚舉單例屬于懶漢式還是餓漢式
// 問題6:枚舉單例如果希望加入一些單例創建時的初始化邏輯該如何做
enum Singleton {
INSTANCE;
}
- 問題1:isntance是enum中的靜態成員變量,是唯一的。
- 問題2:是靜態變量在類加載時創建。
- 問題3:反射不能破壞枚舉單例。
- 問題4:枚舉已經實現了序列化接口,因此反序列化不會破壞單例。
- 問題5:餓漢式。
- 問題6:在枚舉中添加構造方法和普通方法。
(3)懶漢式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析這里的線程安全, 并說明有什么缺點
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
在getinstance方法上添加了synchronized關鍵字,可以保證線程安全,但鎖的范圍太大,每次調用getisntance方法都需要在類對象上加鎖,效率低。
(4)(3)的改進,雙重檢查鎖
public final class Singleton {
private Singleton() { }
// 問題1:解釋為什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 問題2:對比實現3, 說出這樣做的意義
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 問題3:為什么還要在這里加為空判斷, 之前不是判斷過了嗎
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
- 問題1:實現讀寫屏障,防止由于指令重排序造成未創建完成的單例對象逸出。
- 問題2:通過縮小鎖的范圍,提高效率。
- 問題3:當多個線程都進入getinstance方法,都執行完判斷后就切換到其他線程,那么就會出現多個線程等待獲得類對象鎖,如果不判斷,會出現多次創建對象,違反單例。
(5)靜態內部類
public final class Singleton {
private Singleton() { }
// 問題1:屬于懶漢式還是餓漢式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 問題2:在創建時是否有并發問題
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
- 問題1:屬于懶漢式,因為類的加載就是懶惰的,只有用到的類才會加載到jvm中。
- 問題2:不會,類的加載由jvm自己來保證線程安全性,因此不會產生并發問題。
三、總結
單例的優點:
- 在內存里只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例
- 避免對資源的多重占用(比如寫文件操作)。
缺(que)點:沒(mei)有接口(kou),不能繼(ji)承,與(yu)單(dan)一職責原則沖突(tu),一個類應該只關(guan)心內部邏輯,而不關(guan)心外面怎(zen)么樣來實(shi)例(li)化。
應當根據(ju)不同的(de)使(shi)用場景(jing),使(shi)用不同的(de)實現方式。