
面試題:寫一個你認為最好的單例模式
面試考察點
考察目的: 單例模式可以考察非常多的基礎知識,因此對于這種問題,很多面試官都會問, 小伙伴要注意,在面試程序中,但凡能夠從多個維度考察求職者能力的題目,一定不會被拋棄,特別是比較泛的問題,比如: ”請你說說對xxx的理解“之類,
考察范圍: 作業1到5年經驗,隨著經驗的提升,對于該問題的考察深度越深,
背景知識
單例模式,是一種軟體設計模式,屬于創建型模式的一種,
它的特性是:保證一個類只有唯一的一個實體,并提供一個全域的訪問點,
基于這個特性可以知道,單例模式的好處是,可以避免物件的頻繁創建對于記憶體的消耗,因為它限制了實體的創建,總的來說,它有以下好處:
-
控制資源的使用,通過執行緒同步來控制資源的并發訪問;
-
控制實體產生的數量,達到節約資源的目的,
-
作為通信媒介使用,也就是資料共享,它可以在不建立直接關聯的條件下,讓多個不相關的兩個執行緒或者行程之間實作通信,
在實際應用中,單例模式使用最多的就是在Spring的IOC容器中,對于Bean的管理,默認都是單例,一個bean只會創建一個物件,存在內置map中,之后無論獲取多少次該bean,都回傳同一個物件,
下面來了解單例模式的設計,
單例模式設計
既然要保證一個類在運行期間只有一個實體,那必然不能使用new關鍵字來進行實體,
所以,第一步一定是私有化該類的構造方法,這樣就防止了呼叫方自己創建該類的實體,
接著,由于外部無法實體化該物件,因此必須從內部實體化之后,提供一個全域的訪問入口,來獲取該類的全域唯一實體,因此我們可以在類的內部定義一個靜態變數來參考唯一的實體,作為對外提供的實體訪問物件,基于這些點,我們可以得到如下設計,
public class Singleton {
// 靜態欄位參考唯一實體:
private static final Singleton INSTANCE = new Singleton();
// private構造方法保證外部無法實體化:
private Singleton() {
}
}
接著,還需要給外部一個訪問該物件實體INSTANCE的方法,我們可以提供一個靜態方法
public class Singleton {
// 靜態欄位參考唯一實體:
private static final Singleton INSTANCE = new Singleton();
// 通過靜態方法回傳實體:
public static Singleton getInstance() {
return INSTANCE;
}
// private構造方法保證外部無法實體化:
private Singleton() {
}
}
這樣就完成了單例模式的設計,總結來看,單例模式分三步驟,
- 使用
private私有化構造方法,確保外部無法實體化; - 通過
private static變數持有唯一實體,保證全域唯一性; - 通過
public static方法回傳此唯一實體,使外部呼叫方能獲取到實體,
單例模式的其他實作
既然單例模式只需要保證程式運行期間只會產生唯一的實體,那意味著單例模式還有更多的實作方法,
- 懶漢式單例模式
- 餓漢式單例模式
- DCL雙重檢查式單例
- 靜態內部類
- 列舉單例
- 基于容器實作單例
懶漢式單例模式
懶漢式,表示不提前創建物件實體,而是在需要的時候再創建,代碼如下,
public class Singleton {
private static Singleton instance;
private Singleton() {
}
// synchronized方法,多執行緒情況下保證單例物件唯一
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
其中,對getInstance()方法,增加了synchronized同步關鍵字,目的是為了避免在多執行緒環境下同一時刻呼叫該方法導致出現多實體問題(執行緒的并行執行特性帶來的執行緒安全性問題),
優點: 只有在使用時才會實體化單例,一定程度上節約了記憶體資源,
缺點: 第一次加載時要立即實體化,反應稍慢,每次呼叫getInstance()方法都會進行同步,這樣會消耗不必要的資源,這種模式一般不建議使用,
DCL雙重檢查式單例
DCL雙重檢查式單例模式,是基于餓漢式單例模式的性能優化版本,
/**
* DCL實作單例模式
*/
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
// 兩層判空,第一層是為了避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {// 第二層是為了在null的情況下創建實體
instance = new Singleton();
}
}
}
return instance;
}
}
從代碼中可以看到,DCL模式做了兩處改進:
-
在
getInstance()方法中,把synchronized同步鎖的加鎖范圍縮小了,縮小鎖的范圍能夠帶來性能上的提升,不妨思考一下,在原來的
懶漢式模式中,把synchronized關鍵字加載方法級別上,意味著不管是多執行緒環境還是單執行緒環境,任何一個呼叫者需要獲得這個物件實體時,都需要獲得鎖,但是加這個鎖其實只有在第一次初始化該實體的時候起到保護作用,后續的訪問,應該直接回傳instance實體物件就行,所以把synchroinzed加在方法級別,在多執行緒環境中必然會帶來性能上的開銷,而DCL模式的改造,就是縮小了加鎖的范圍,只需要保護該實體物件
instance在第一次初始化即可,后續的訪問,都不需要去競爭同步鎖,因此它的設計是:- 先判斷
instance實體是否為空,如果是,則增加synchronized類級別鎖,保護instance物件的實體化程序,避免在多執行緒環境下出現多實體問題, - 接著再
synchronized同步關鍵字范圍內,再一次判斷instance實體是否為空,同樣也是為了避免臨界點時,上一個執行緒剛初始化完成,下一個執行緒進入到同步代碼塊導致多實體問題,
- 先判斷
-
在成員變數
instance上修飾了volatile關鍵字,該關鍵字是為了保證可見性,之所以要加這個關鍵字,是為了避免在JVM中指令重排序帶來的可見性問題,這個問題主要體現在
instance=new Singleton()這段代碼中,我們來看這段代碼的位元組碼17: new #3 // class org/example/cl04/Singleton 20: dup 21: invokespecial #4 // Method "<init>":()V 24: putstatic #2 // Field instance:Lorg/example/cl04/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0關注以下幾個指令
-
new #3 : 這行指令是說在堆上的某個地址處開辟了一塊空間作為Singleton物件
-
invokespecial #4 :這行指令是說將物件里的成員變數進行賦值操作
-
astore_1 :這行指令是說將堆疊里的Singleton instance與堆上的物件建立起參考關聯
而
invokespecial #4指令,和astore_1指令,是允許重排序的(關于重排序問題,就不再本篇文章中說明,后續的面試題中會分析到),就是說執行順序有可能astore_1先執行,invokespecial #1后執行,重排序對于兩個沒有依賴關系的指令操作,CPU和記憶體以及JVM,為了優化程式執行性能,會對執行指令進行重排序,也就是說兩個指令的執行順序不一定會按照程式撰寫順序來執行,
因為在堆上建立物件開辟地址以后,地址就已經定了,而
“將堆疊里的Singleton instance與堆上的物件建立起參考關聯” 和 “將物件里的成員變數進行賦值操作” 是沒什么邏輯關系的,所以cpu可以進行亂序執行,只要程式最終的結果是一致的就可以,
這種情況,在單執行緒下沒有問題,但是多執行緒下,就會出現錯誤,
試想一下,DCL下,執行緒A在將物件new出來的時,剛執行完
new #4指令,緊接著沒有執行invokespecial #4指令,而是執行了astore_1,也就是說發生了指令重排序,此時執行緒B進入getInstance(),發現instance并不為空(因為已經有了參考指向了物件,只不過還沒來得及給物件里的成員變數賦值),然后執行緒B便直接return了一個“半初始化”物件(物件還沒徹底創建完),
所以DCL里,需要給instance加上volatile關鍵字,因為volatile在JVM層有一個特性叫記憶體屏障,可以防止指令重排序,從而保證了程式的正確性,
-
關于DCL模式的優缺點:
優點:資源利用率高,既能夠在需要的時候才初始化實體,又能保證執行緒安全,同時呼叫getInstance()方法不進行同步鎖,效率高,
缺點:第一次加載時稍慢,由于Java記憶體模型的原因偶爾會失敗,在高并發環境下也有一定的缺陷,雖然發生概率很小,
DCL模式是使用最多的單例模式實作方式,除非代碼在并發場景比較復雜,否則,這種方式基本都能滿足需求,
餓漢式單例模式
在類加載的時候不創建單例實體,只有在第一次請求實體的時候的時候創建,并且只在第一次創建后,以后不再創建該類的實體,
/**
* 餓漢式實作單例模式
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
由于static關鍵字修飾的屬性,表示這個成員屬于類本身,不屬于實體,運行時,Java 虛擬機只為靜態變數分配一次記憶體,在類加載的程序中完成靜態變數的記憶體分配,
所以在類加載的時候就創建好物件實體,后續在訪問時直接獲取該實體即可,
而該模式的優缺點也非常明顯,
優點:執行緒安全,不需要考慮并發安全性,
缺點:浪費記憶體空間,不管該物件是否被使用到,都會在啟動是提前分配記憶體空間,
靜態內部類
靜態內部類,是基于餓漢式模式下的優化,
第一次加載Singleton類時不會初始化instance,只有在第一次呼叫getInstance()方法時,虛擬機會加載SingletonHolder類,初始化instance,instance 的唯一性、創建程序的執行緒安全性,都由 JVM 來保證,
/**
* 靜態內部類實作單例模式
*/
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
/**
* 靜態內部類
*/
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
}
這種方式既保證執行緒安全,單例物件的唯一,也延遲了單例的初始化,推薦使用這種方式來實作單例模式,
靜態內部類不會因為外部內的加載而加載,同時靜態內部類的加載不需要依附外部類,在使用時才加載,不過在加載靜態內部類的程序中也會加載外部類
知識點:如果用static來修飾一個內部類,那么就是靜態內部類,這個內部類屬于外部類本身,但是不屬于外部類的任何物件,因此使用static修飾的內部類稱為靜態內部類,靜態內部類有如下規則:
- 靜態內部類不能訪問外部類的實體成員,只能訪問外部類的類成員,
- 外部類可以使用靜態內部類的類名作為呼叫者來訪問靜態內部類的類成員,也可以使用靜態內部類物件訪問其實體成員,
靜態內部類單例優點:
- 物件的創建是執行緒安全的,
- 支持延時加載,
- 獲取物件時不需要加鎖,
這是一種比較常用的模式之一,
基于列舉實作單例
用列舉來實作單例,是最簡單的方式,這種實作方式通過Java列舉型別本身的特性,保證了實體創建的執行緒安全性和實體的唯一性,
public enum SingletonEnum {
INSTANCE;
public void execute(){
System.out.println("begin execute");
}
public static void main(String[] args) {
SingletonEnum.INSTANCE.execute();
}
}
基于列舉實作單例會發現它并不需要前面描述的幾個操作
- 構造方法私有化
- 實體化的變數參考私有化
- 獲取實體的方法共有
這類的方式實作列舉其實并不保險,因為私有化構造并不能抵御反射攻擊.
這種方式是
Effective Java作者Josh Bloch提倡的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新創建新的物件,可謂是很堅強的壁壘啊,
基于容器實作單例
下面的代碼演示了基于容器的方式來管理單例,
import java.util.HashMap;
import java.util.Map;
/**
* 容器類實作單例模式
*/
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
public static void regsiterService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
SingletonManager可以管理多個單例型別,在程式的初始化時,將多個單例型別注入到一個統一管理的類中,使用時根據key獲取物件對應型別的物件,這種方式可以通過統一的介面獲取操作,隱藏了具體實作,降低了耦合度,
關于單例模式的破壞
前面在分析列舉類實作單例模式時,有提到一個問題,就是私有化構造,會被反射破壞,導致出現多實體問題,
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
// 兩層判空,第一層是為了避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {// 第二層是為了在null的情況下創建實體
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception{
Singleton instance=Singleton.getInstance();
Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton refInstance=constructor.newInstance();
System.out.println(instance);
System.out.println(refInstance);
System.out.println(instance==refInstance);
}
}
運行結果如下
org.example.cl04.Singleton@29453f44
org.example.cl04.Singleton@5cad8086
false
由于反射可以破壞private特性,所以凡是通過private私有化構造實作的單例模式,都能夠被反射破壞從而出現多實體問題,
可能有人會問,我們沒事干嘛要去破壞單例呢? 直接基于這個入口訪問就不會有問題啊?
理論上來說是這樣,但是,假設遇到下面這種情況呢?
下面的代碼演示的是通過物件流實作Singleton的序列化和反序列化,
public class Singleton implements Serializable {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
// 兩層判空,第一層是為了避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {// 第二層是為了在null的情況下創建實體
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception {
Singleton instance=Singleton.getInstance();
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(instance);
ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bais);
Singleton ri=(Singleton) ois.readObject();
System.out.println(instance);
System.out.println(ri);
System.out.println(instance==ri);
}
}
運行結果如下
org.example.cl04.Singleton@36baf30c
org.example.cl04.Singleton@66a29884
false
可以看到,序列化的方式,也會破壞單例模式,
列舉類單例的破壞測驗
可能有人會問,列舉難道就不能破壞嗎?
我們可以試試看,代碼如下,
public enum SingletonEnum {
INSTANCE;
public void execute(){
System.out.println("begin execute");
}
public static void main(String[] args) throws Exception{
SingletonEnum instance=SingletonEnum.INSTANCE;
Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonEnum refInstance=constructor.newInstance();
System.out.println(instance);
System.out.println(refInstance);
System.out.println(instance==refInstance);
}
}
運行結果如下
Exception in thread "main" java.lang.NoSuchMethodException: org.example.cl04.SingletonEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at org.example.cl04.SingletonEnum.main(SingletonEnum.java:15)
從錯誤來看,似乎是沒有一個空的建構式?這里并沒有證明 反射無法破壞單例,
下面是Enum這類的原始碼,所有列舉類都繼承了Enum這個抽象類,
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
/**
* The name of this enum constant, as declared in the enum declaration.
* Most programmers should use the {@link #toString} method rather than
* accessing this field.
*/
private final String name;
/**
* Returns the name of this enum constant, exactly as declared in its
* enum declaration.
*
* <b>Most programmers should use the {@link #toString} method in
* preference to this one, as the toString method may return
* a more user-friendly name.</b> This method is designed primarily for
* use in specialized situations where correctness depends on getting the
* exact name, which will not vary from release to release.
*
* @return the name of this enum constant
*/
public final String name() {
return name;
}
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;
/**
* Returns the ordinal of this enumeration constant (its position
* in its enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this method. It is
* designed for use by sophisticated enum-based data structures, such
* as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*
* @return the ordinal of this enumeration constant
*/
public final int ordinal() {
return ordinal;
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
}
該類有一個唯一的構造方法,接受兩個引數分別是:name和ordinal
那我們嘗試通過這個構造方法來創建一下實體,演示代碼如下,
public enum SingletonEnum {
INSTANCE;
public void execute(){
System.out.println("begin execute");
}
public static void main(String[] args) throws Exception{
SingletonEnum instance=SingletonEnum.INSTANCE;
Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
SingletonEnum refInstance=constructor.newInstance("refinstance",2);
System.out.println(instance);
System.out.println(refInstance);
System.out.println(instance==refInstance);
}
}
運行上述代碼,執行結果如下
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at org.example.cl04.SingletonEnum.main(SingletonEnum.java:17)
從錯誤資訊來看,我們成功獲取到了Constructor這個構造器,但是在newInstance時報錯,
定位到出錯的原始碼位置,
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
從這段代碼:(clazz.getModifiers() & Modifier.ENUM) != 0說明:反射在通過newInstance創建物件時,會檢查該類是否ENUM修飾,如果是則拋出例外,反射失敗,因此列舉型別對反射是絕對安全的,
既然反射無法破壞?那序列化呢?我們再來試試
public enum SingletonEnum {
INSTANCE;
public void execute(){
System.out.println("begin execute");
}
public static void main(String[] args) throws Exception{
SingletonEnum instance=SingletonEnum.INSTANCE;
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(instance);
ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bais);
SingletonEnum ri=(SingletonEnum) ois.readObject();
System.out.println(instance);
System.out.println(ri);
System.out.println(instance==ri);
}
}
運行結果如下.
INSTANCE
INSTANCE
true
因此,我們可以得出一個結論,列舉型別是所有單例模式中唯一能夠避免反射破壞導致多實體問題的設計模式,
綜上,可以得出結論:列舉是實作單例模式的最佳實踐,畢竟使用它全都是優點:
-
反射安全
-
序列化/反序列化安全
-
寫法簡單
問題解答
面試題:寫一個你認為最好的單例模式
對于這個問題,相比大家都有答案了,列舉方式實作單例才是最好的,
當然,回答的時候要從全方面角度去講解,
- 單例模式的概念
- 有哪些方式實作單例
- 每種單例模式的優缺點
- 最好的單例模式,以及為什么你覺得它是最好的?
問題總結
單例模式看起來簡單,但是學到極致,也還是有很多知識點的,
比如涉及到執行緒安全問題、靜態方法和靜態從成員變數的特征、列舉、反射等,
多想再回到從前,大家都只用jsp/servlet,沒有這么多亂七八糟的知識,我們只想做個簡單的程式員,
關注[跟著Mic學架構]公眾號,獲取更多精品原創

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/346857.html
標籤:Java
