1. 程式人生 > >面試官:請寫一個你認為比較“完美”的單例

面試官:請寫一個你認為比較“完美”的單例

單例模式是保證一個類的例項有且只有一個,在需要控制資源(如資料庫連線池),或資源共享(如有狀態的工具類)的場景中比較適用。如果讓我們寫一個單例實現,估計絕大部分人都覺得自己沒問題,但如果需要實現一個比較完美的單例,可能並沒有你想象中簡單。本文以主人公小雨的一次面試為背景,循序漸進地討論如何實現一個較為“完美”的單例。本文人物與場景皆為虛構,如有雷同,純屬捏造。 小雨計算機專業畢業三年,對設計模式略有涉獵,能寫一些簡單的實現,掌握一些基本的JVM知識。在某次面試中,面試官要求現場寫程式碼:請寫一個你認為比較“完美”的單例。 ## 簡單的單例實現 憑藉著對單例的理解與印象,小雨寫出了下面的程式碼 ```java public class Singleton { private static Singleton instance; private Singleton(){} public static final Singleton getInstance(){ if(instance == null) { instance = new Singleton(); } return instance; } } ``` 寫完後小雨審視了一遍,總覺得有點太簡單了,離“完美”貌似還相差甚遠。對,在多執行緒併發環境下,這個實現就玩不轉了,如果兩個執行緒同時呼叫 getInstance() 方法,同時執行到了 if 判斷,則兩邊都認為 instance 例項為空,都會例項化一個 Singleton 物件,就會導致至少產生兩個例項了,小雨心想。嗯,需要解決多執行緒併發環境下的同步問題,保證單例的執行緒安全。 ## 執行緒安全的單例 一提到併發同步問題,小雨就想到了鎖。加個鎖還不簡單,synchronized 搞起, ```java public class Singleton { private static Singleton instance; private Singleton(){} public synchronized static final Singleton getInstance(){ if(instance == null) { instance = new Singleton(); } return instance; } } ``` 小雨再次審視了一遍,發現貌似每次 getInstance() 被呼叫時,其它執行緒必須等待這個執行緒呼叫完才能執行(因為有鎖鎖住了嘛),但是加鎖其實是想避免多個執行緒同時執行例項化操作導致產生多個例項,在單例被例項化後,後續呼叫 getInstance() 直接返回就行了,每次都加鎖釋放鎖造成了不必要的開銷。 經過一陣思索與回想之後,小雨記起了曾經看過一個叫 Double-Checked Locking 的東東,雙重檢查鎖,嗯,再優化一下, ```java public class Singleton { private static volatile Singleton instance; private Singleton(){} public static final Singleton getInstance(){ if(instance == null) { synchronized (Singleton.class){ if(instance == null) { instance = new Singleton(); } } } return instance; } } ``` 單例在完成第一次例項化,後續再呼叫 getInstance() 先判空,如果不為空則直接返回,如果為空,就算兩個執行緒同時判斷為空,在同步塊中還做了一次雙重檢查,可以確保只會例項化一次,省去了不必要的加鎖開銷,同時也保證了執行緒安全。並且令小雨感到自我滿足的是他基於對JVM的一些瞭解加上了 volatile 關鍵字來避免例項化時由於指令重排序優化可能導致的問題,真是畫龍點睛之筆啊。 簡直——完美! > Tips: volatile關鍵字的語義 1. 保證變數對所有執行緒的可見性。對變數寫值的時候JMM(Java記憶體模型)會將當前執行緒的工作記憶體值重新整理到主記憶體,讀的時候JMM會從主記憶體讀取變數的值而不是從工作記憶體讀取,確保一個變數值被一個執行緒更新後,另一個執行緒能立即讀取到更新後的值。 2. 禁止指令重排序優化。JVM在執行程式時為了提高效能,編譯器和處理器常常會對指令做重排序,使用 volatile 可以禁止進行指令重排序優化。 > JVM建立一個新的例項時,主要需三步: 1. 分配記憶體 2. 初始化構造器 3. 將物件引用指向分配的記憶體地址 > 如果一個執行緒在例項化時JVM做了指令重排,比如先執行了1,再執行3,最後執行2,則另一個執行緒可能獲取到一個還沒有完成初始化的物件引用,呼叫時可能導致問題,使用volatile可以禁止指令重排,避免這種問題。 小雨將答案交給面試官,面試官瞄了一眼說道:“基本可用了,但如果我用反射直接呼叫這個類的建構函式,是不是就不能保證單例了。” 小雨撓撓頭,對哦,如果使用反射就可以在執行時改變單例構造器的可見性,直接呼叫構造器來建立一個新的例項了,比如通過下面這段程式碼 ```java Constructor