看了這篇文章,你能和麪試官暢談單例模式
一、前言
最近看了很多的書還有視頻,他們都花了很長的篇幅提到了單例模式,於是我想把他們都總結起來,寫下這篇文章。目的就是,讓小白能搞懂單例模式,以及單例模式的經典面試題。爲什麼說是小白也能懂的呢?哈哈哈,還不是小胖也是一個小白~~~
二、單例模式的解釋
單例模式定義:一個類只能有一個實例,且該類能自行創建這個實例的一種模式。其實單例模式在C#或者.NET裏面更好理解,像win7的任務管理器,在系統中只能創建一個。有些理解了嘛?
單例模式只能有一個實例,實例化其實就是new的過程,是不可能阻止他人不去用new的。所以我們完全可以直接就把這個類的構造方法改成私有的。對於外部的代碼,不能用new來實例化他,我們完全可以再寫一個public方法,叫做getInstance(),這個方法的目的就是返回一個實例,但是在這個方法中,我們需要是否實例化的判斷。
來一個簡單的例子
package singleton;
/**
* 描述: 懶漢式(線程不安全)
* **/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){
}
//如果兩個線程同時到達,會出現線程不安全的情況
public static Singleton3 getInstance(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
單例模式,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。可以節省內存和計算、保證結果正確、方便管理。適用場景是無狀態的工具類、全局信息類。
記住,上面的單例模式是線程不安全的。
三、實現單例模式的8種寫法
1.餓漢式(靜態常量)(可用)
package singleton;
/***
* 描述:餓漢式(靜態常量) (可用)
* **/
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
上面是餓漢式的靜態常量的寫法,可以看到類在加載後就完成了實例化的創建。優點:寫法簡單,類加載後就完成了實例的創建。缺點:提前佔用系統的資源。
2.餓漢式(靜態代碼塊)(可用)
package singleton;
/***
* 描述:餓漢式(靜態代碼塊) (可用)
* **/
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2(){
}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
上面是餓漢式的靜態代碼塊的方式,只不過和第一種有一些區別。優缺點和第一種是一樣的。優點:寫法簡單,類加載後就完成了實例的創建。缺點:提前佔用系統的資源。
3.懶漢式(線程不安全)(不可用)
package singleton;
/**
* 描述: 懶漢式(線程不安全)
* **/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){
}
//如果兩個線程同時到達,會出現線程不安全的情況
public static Singleton3 getInstance(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
上面的懶漢式的寫法,它是線程不安全的,因爲在多線程的時候,可能會有兩個線程同時到達instance==null,然後都會初始化,這就創建了兩個對象了,是線程不安全的,不能使用。
4.懶漢式(線程安全)(不推薦)
package singleton;
/**
* 描述:懶漢式(線程安全)(不推薦)
* */
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
//但是效率不高
public synchronized static Singleton4 getInstance(){
if (instance == null){
instance = new Singleton4();
}
return instance;
}
}
上面是懶漢式的第二種寫法,這種方法是線程安全的,但是不推薦使用,因爲效率是低下的。在getInstance上面加上了synchronized的同步方法,那麼只能有一個線程可以進入到這個方法中,但是在多線程的時候效率是非常低的,因爲任何一個線程進入的這個方法,都需要去等待鎖的釋放,所以不推薦使用。
5.懶漢式(線程不安全)(不可用)
package singleton;
/**
* 描述:懶漢式(線程不安全) (不推薦)
* */
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){
}
public static Singleton5 getInstance(){
if (instance == null){
synchronized (Singleton5.class){
instance = new Singleton5();
}
}
return instance;
}
}
上面是懶漢式的第三種寫法,是對第二種的改進,不在鎖在方法上,加在創建對象上面,但是這可能會引發線程安全的問題。如果連個線程都判斷instance==null,都進入到if裏面,這時只有一個線程會運行,但是第一個線程執行完成之後,第二個線程還是會創建實例,那麼就是線程不安全的。
6.懶漢式(雙重檢查)(推薦面試使用)
package singleton;
/**
* 描述: 雙重檢查
* 優點: 線程安全;延遲加載;效率較高
* 爲什麼要double-check
* 1.線程安全
* 2.單check爲什麼不行?
* 3.放在判斷後面會引發線程安全問題
* 4.單層鎖,但是synchronized放在方法上,這樣可以,但是會導致性能問題
*
* 爲什麼要用volatile
* 1.新建對象實際上有3個步驟(分配內存資源,調用構造函數,將對象指向分配的內存空間)新建對象不是原子操作。
* 2.JVM重排序會帶來NPE(空指針的問題)
* 3.防止重排序
* */
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6(){
}
public static Singleton6 getInstance(){
if (instance == null){
synchronized (Singleton6.class){
if (instance == null){
instance = new Singleton6();
}
}
}
return instance;
}
}
上面就是很有名的double-check單例模式了,它的優點有:線程安全;延遲加載;效率較高。它是線程安全的,而且效率很高,推薦我們在面試的時候使用。這個代碼同時也引出了我們在面試過程中的2個問題。
懶漢式單例模式爲什麼要用double-check,不用就不安全嗎?懶漢式單例模式爲什麼雙重檢查模式要用volatile?
7.懶漢(靜態內部類方式)(可用)
package singleton;
/**
* 描述: 靜態內部類方式,可用
* 懶漢
* ***/
public class Singleton7 {
private Singleton7(){
}
private static class SingletonInstance{
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}
上面是靜態內部類的方式,是可以推薦使用的,而且效率也是可以的。外部類加載,JVM不會創建多個實例。
8.枚舉 推薦在項目中使用
package singleton;
/**
* 描述:單例模式:枚舉 推薦用
* */
public enum Singleton8 {
INSTANCE;
public void whatever(){
//無論什麼方法
}
}
上面是枚舉方式的單例模式,是生產實踐中最佳的單例模式的寫法,同時可以防止反序列化破壞單例。
四、常見面試問題
什麼叫單例設計模式
答:單例模式的重點在於整個系統上共享一些創建時較耗資源的對象。整個應用只維護一個特定類實例,它被所有組件共同使用。Java.lang.Runtime是單例模式的經典例子。
你知道餓漢式的缺點嗎?
答:餓漢模式,類一加載的時候就會實例化對象,所以要提前佔用系統資源。
那懶漢式的缺點呢?
答:不會出現佔用資源的問題,但是需要使用合適,否則會帶來線程安全問題。
懶漢式單例模式爲什麼要用double-check,不用就不安全嗎?
答:爲了線程安全,我們需要使用double-check。
追問,單check爲什麼不行?(代碼見5.懶漢式)
答:單check是線程不安全的(代碼見5.懶漢式),可能會有多個線程走到了 if (instance == null)的裏面,由於synchronized看似只能有一個線程會創建對象,但是第二個也會創建。
追問,你可以把synchronized寫在方法的外面呀?(代碼見4,。懶漢式)
答:這個是可以解決線程安全的問題,但是效率不是很高,每個線程都需要等待鎖的釋放,會導致性能的問題,不推薦使用。
你說爲什麼要用volatile?
答:在多線程的時候,創建對象分爲3步,CPU可能會重排序,首先建一個空的對象,然後複製給引用,然後調用構造方法。
第一個線程進來了,第一個對象已經不是空的,但是構造方法沒有執行,裏面的屬性是空的。第二個線程發現不是空的,就會直接跳過創建實例的方法,之後再使用的時候引發的問題。使用volatile可以避免這個問題,對於第二個線程來說,他的創建過程對第一個線程來說是可見了,他就會等待創建完成。
那這麼多應該如何選擇,用哪種單例的實現方案最好?
答:枚舉方式的單例模式,是生產實踐中最佳的單例模式的寫法,同時可以防止反序列化破壞單例。
那你知道happens-before原則嘛?
答:volatile就是happens-before原則呀…未完待續
請用Java寫出線程安全的單例模式。
答:上面已經很多例子了,小胖覺得可以用第6種double-check。
五、關於幾種解法的選擇
《劍指offer》上面的推薦給面試官的解法是1.餓漢式(靜態常量)(可用)和7.懶漢(靜態內部類方式)(可用)。
《大話設計模式》上面的推薦也是1.餓漢式(靜態常量)(可用)
《線程八大核心+Java併發底層原理精講》上面推薦使用6.懶漢式(雙重檢查)(推薦面試使用)
小胖覺得可以使用第6種,這可能會打開一些面試的問題,把問題引入到我們瞭解熟悉的方向。當然你在手寫單例模式的時候,可以去詢問一下要求,是需要餓漢式還是懶漢式。
六、參考資料
書籍1:《大話設計模式》 第21章 有些類也需計劃生育–單例模式
書籍2:《劍指offer》 面試題2:實現Singleton模式
視頻:《線程八大核心+Java併發底層原理精講》
七、關於本系列的解釋
本系列想製作23種設計模式+7種設計原則一系列課程,其目的就是一個簡單的記錄學習的過程。不知道能幫助到多少人,也不知道技術是否會有一定的深度。
製作不易,您的點贊是我最大的動力。希望我們都能成爲想成爲的人
希望我們都能成爲想成爲的人