《巫師3》中,陪着主人公南征北戰的坐騎,不管你何時何地召喚它,它永遠隻有一個名字——蘿蔔。
大家好,我是左耳朵梵高。文章首發于微信公衆号「左耳朵梵高」,歡迎關注,和我一起持續學習,終身成長。 ---- 生活不隻眼前的苟且,還有詩和遠方。
HR :來了一個面試Java的,我讓他在小會議室等着了。
面試官 :好的,我就來。
面試官用一次性紙杯倒了杯水,夾着Mac,進了小會議室。看見一個20出頭的精神小夥,帶着黑框眼鏡,發量誘人,像極了N年前的自己,風華正茂,書生意氣。
面試官 :你好,先喝杯水吧。(不給應聘者倒水的公司都是不靠譜的)我看你履歷上寫着精通設計模式,要不我們就聊聊設計模式吧。
應聘者 :可以呀。
一句輕描淡寫的“可以呀”,但經驗豐富的面試官還是發現了平靜面容下,應聘者的一絲絲竊喜,好像很胸有成竹的樣子。
面試官 :那就說說,你平時都用了哪些設計模式吧?
應聘者 :(内心狂喜ing)我平時使用最多的設計模式有單例模式。單例模式屬于23種設計模式中的建立型的設計模式。23種設計模式可以分為3種:建立型、結構型和行為型。單例模式確定了一個類隻有一個執行個體。單例模式有5種實作方式:懶漢式、餓漢式、Double-Check方式、靜态内部類方式、枚舉方式。
面試官 :嗯,你對單例模式了解的不錯嘛。你先說下為什麼要使用單例模式吧。
應聘者 :單例模式其實很簡單,就是一個類隻能建立一個執行個體。在程式中,有一些對象隻需要一個,比如說:線程池、緩存、對話框、系統資料庫、日志對象、充當列印機、顯示卡等裝置驅動程式的對象。事實上,這一類對象隻能有一個執行個體,如果制造出多個執行個體就可能會導緻一些問題的産生,比如:程式的行為異常、資源使用過量、或者不一緻性的結果。還有些業務上就隻會有一個,比如公司主體等。
面試官 :那如何實作一個單例呢?
應聘者 :實作單例有好幾種方式,有餓漢式、懶漢式、靜态内部類,或者使用枚舉來實作。使用單例模式,一般把類的構造函數設定為private,避免通過new建立多個示例。我先來說下餓漢方式吧。
應聘者喝了口水,似乎準備開始表演了。
應聘者 :餓漢式實作比較簡單。類有一個靜态的執行個體,一般取名為instance。在類加載的時候,就會建立并初始化好instance執行個體。是以,餓漢式是線程安全的。
面試官 :你能寫一下具體的實作代碼嗎?
應聘者很快就在紙上寫出了餓漢式的代碼實作:
看的出來,應聘者對餓漢式的代碼實作很熟悉,編碼風格和命名也很不錯。我在面試的時候,就有幾位應聘者不知道如何給類命名,有的使用Danli,有的使用Single或One。
面試官 :嗯,很不錯。你平時都使用這種方式嗎?
應聘者 :哦,不是的。餓漢式雖然簡單,但是有個問題是,它不支援延遲加載,或者叫按需加載。在系統啟動時,就必須要建立執行個體。
面試官 :這樣會有什麼問題嗎?
應聘者 :如果執行個體占用資源多,比如記憶體占用高,或者初始化耗時長(比如需要加載各種配置檔案),提前初始化就會造成浪費。應該在用到的時候再去初始化。
面試官 :如果初始化耗時長,等用到的時候再初始化。就可能在使用者請求接口的時候,觸發了這個初始化過程,會導緻請求的響應時間很長,甚至逾時。對使用者造成影響。是以,究竟是啟動時初始化好,還是延遲初始化好呢?
應聘者 :啊,這個。。。(這個面試官不按套路出牌呀)網上說的都是要延遲加載。
面試官 :還有,如果執行個體占用資源多,比如記憶體使用高。如果延遲加載,可能會出現在程式運作一段時間後,因為初始化執行個體,占用資源多,出現了OOM,程式崩潰。根據Fast Fail原則,是不是就應該在啟動時初始化執行個體,如果資源不夠,我們就能快速發現問題,盡快進行修複,而不會讓問題在生産環境中才暴露。
應聘者 :嗯,好像有道理。但我看網上的文章都說這種方式不好。
面試官 :那你覺得哪種方式好呢?
應聘者 :(内心有些搖擺,有些淩亂)
面試官 :那我們再聊聊延遲加載的單例?
應聘者 :嗯嗯,好呀。延遲加載就是在使用的時候才進行初始化,它的代碼實作是這樣的:
面試官 :嗯,不錯嘛。我看getInstance方法中,有多個null判斷,還有個synchronized鎖,能不能解釋一下。
應聘者 :這個叫雙重檢查(Double Check)。加synchronized是為了保證線程安全。null判斷是為了提升性能。如果不在前面先判斷instance是否為null,就需要在每次使用時,先擷取鎖,然後釋放鎖,會導緻性能瓶頸。
應聘者 :是以使用了雙重檢查,隻要instance被建立後,即使再調用getInstance,也不會再加鎖了。解決了性能問題。
應聘者 :網上有人說,這種實作方式也有問題。因為指令重排,可能會導緻Singleton被new出來後,被指派給了instance,還沒來得及初始化,就被另一個線程使用了,可能會出現NPE錯誤。要解決這個問題,我們需要給instance成員變量添加volatile關鍵字,禁止指令重排。
面試官 :嗯,你對Java指令重排也有了解呀,不錯。關于線程安全,我們稍後再仔細聊聊吧。
應聘者 :(不要啊,我就隻記住了這一段。待會兒一聊就露餡了啊。。。)
面試官:你知道還有其它實作單例的方式嗎?
應聘者 :還有個靜态内部類方式。它比雙重檢查更加簡單。就是利用Java的靜态内部類。代碼實作是這樣的:
應聘者 :SingletonHolder是一個内部靜态類,當外部Singleton被加載時,并不會建立SingletonHolder執行個體對象。隻有當調用getInstance方法時,SingletonHolder才被加載,這個時候才會建立instance。instance的唯一性、建立過程的線程安全有JVM虛拟機來保證。是以,這種實作方法既保證了線程安全,又能做到延遲加載。
應聘者 :還有一種使用枚舉建立單例的方式。
面試官 :哇,還有嘛。那你再說說吧。
應聘者 :使用枚舉應該是最簡單的。它利用了Java枚舉類型本身的特點,保證了執行個體建立的線程安全和執行個體唯一性。代碼如下:
面試官 :你平時都是使用這個方式嗎?
應聘者 :沒有呢。這種方式的确簡單,而且也是《Effective Java》作者推薦的。但是我覺得用枚舉來表達一個單例,這種方式比較奇怪。總覺得是一樣投機取巧的方式。
面試官 :哈哈哈。。的确是這樣,開源項目中也很少會使用這種方式,是比較怪。你對單例模式的了解很深入呀,說出了這麼多種實作,不錯不錯。剛才看你對線程安全也挺了解的,那我們接下來再聊聊Java多線程吧。
應聘者 :(狠狠抽了幾下耳巴子。。。叫你多嘴。。。)
單例模式是面試中經常出現的話題。單例模式本身比較簡單,就是一個類隻有一個執行個體。大部分面試者在面試準備時,都會閱讀單例的相關知識點,比如單例模式的多種實作。
但是,希望大家不要僅僅是背誦,還應該多去了解。本文的面試中,面試官問了一個問題,到底是啟動時初始化好,還是延遲加載好呢?這個問題,大家可以自己思考一下。
