談面試時從寫一個單例開始究竟能問多深及終極解決方案

程式設計一生發表於2017-05-14

  看了左瀟龍的《回答阿里社招面試如何準備,順便談談對於Java程式猿學習當中各個階段的建議》這篇文章,在想一個問題,從一個最簡單的問題入手究竟能把問題問多深?下面就模擬一場面試問答,要是我是面試官,大概就只能問到下面的深度了。

LZ的風格,照例跑會兒題。話說週末跟兒子去超市買了一堆零食。兒子作為一個5歲的男子漢,是要保護媽媽,照顧媽媽的。零食也要讓著媽媽。如果你實在不讓,我就自己搶了。於是週一早上我先兒子起床,把零食往包裡塞呀,塞呀,全塞進去了。老公看不下去了,跟我說“你給兒子留兩個果凍”。好吧,得給老公面子,給兒子留了兩個小果凍,大的聖盃果凍就不要想了。但是看著零食就總想吃啊,這樣下面會變成胖紙。於是將所有的零食貢獻出來給同事們分了。一個不想長胖的吃貨是有多麼的糾結~~

  國家領導人不管開什麼會議,我大燕郊必堵車,週四早上8點出門,下午三點到的公司。於是週五堵車的路上自創了一首《一帶一路進京堵車千古版》,附上歌詞和視訊連結:http://www.le.com/ptv/vplay/29454385.html

夏晨冬夜,每日來回一遍

燈火闌珊,每天到家時間

左盼右盼,只待來年燕郊通地鐵

難斷言,還要多少個日月

夜不成眠,大佬在京相見

好壞參半,久違APIC藍

緊閉雙眼,身在車中心遊千里遠

好豔羨,走路人更快過了檢查站

若一帶一路,通燕高速卻要堵

藍天美如碧樹,誰又為你在乎

若一帶一路,能對經濟發展有幫助

也不悔進京如此的堵

此文中內容均為我原創,如需轉載請註明出處:http://www.cnblogs.com/xiexj/p/6845029.html

謝謝哦~~

 

旁白:一般的面試都是從最簡單基本的問題開始。

面試官:請在黑板上寫出一個執行緒安全的單例模式的例子。

面試者:

  其實執行緒安全的實現有很多種,根據業務場景可以new一個例項作為私有靜態成員變數,這樣程式一啟動,例項就生成,私有化建構函式,利用公用的靜態函式getInstance返回例項。這種預載入的是能保證執行緒安全的但是如果不是確定會被使用,會造成記憶體的浪費,所以可以將例項放到私有靜態類中作為成員變數。下面只寫一種利用鎖機制來保證的懶載入方法。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

旁白:從這個例子上我能想到的知識點主要有三個

     ☆ volatile 關鍵字,可深入到Java VM記憶體相關

     ☆ synchronized 關鍵字,可深入到Java鎖機制,高併發相關

     ☆ new 關鍵字,可深入到Java VM類載入機制相關

但是面試官一開始可能要先考察一下面試者是否真的理解自己寫的程式碼

面試官:你寫的這個程式是怎麼保證執行緒安全的?

面試者:將類的構造方法私有起來,外部呼叫進行初始化的時候只能通過呼叫getSingleton這個靜態方法來獲得例項,靜態方法是整個Java虛擬機器中只有一個例項。在建立的時候首先進行非空判斷,這時候如果例項不存在,對整個類進行加鎖同步,為了避免過程中非空狀態的改變,同步塊內再進行一次判斷,如果不存在例項則建立例項返回。使用volatile關鍵字,下次訪問這個方法就能直接看到例項的最新非空狀態,直接返回例項。

面試官:volatile 起到了什麼作用?

面試者:volatile這個英文單詞意思是易變的,用在多執行緒中來同步變數。Java的物件都是在記憶體堆中分配空間。但是Java有主記憶體和執行緒自己獨有的記憶體拷貝。對於沒有volatile修飾的區域性變數,執行緒在執行過程中訪問的是工作記憶體中的變數值,其修改對於主記憶體不是立即可見。而volatile修飾的值線上程獨有的工作記憶體中無副本,執行緒直接和主記憶體互動,修改對主記憶體立即可見。

面試官:synchronized起到了什麼作用?

面試者:鎖定物件,限制當前物件只能被一個執行緒訪問。

面試官:synchronized裡你傳Singleton.class這個引數,起到什麼作用,換成別的行不行?

面試者:對當前類加鎖,使得這個程式碼塊一次只能被一個執行緒訪問。這裡Singleton.class可以換成一個常量字串或者自己定義一個內部靜態Object。

面試官:那傳Singleton.class,常量字串,自己定義一個內部靜態Object有區別嗎?

面試者:因為這是一個靜態方法,相當於一個概念上的類鎖,所以在這裡起到的效果是一樣的。但是如果是原型模式,或者直接每個類都是new出來的,例項不同的話,在一個非靜態方法里加這三種鎖,這時是一個物件鎖,因為Singleton.class或者是靜態的一個Object或者是JVM只存一份的字串常量,這些物件執行緒間是共享的,會對所有的例項的同步塊都加同一把鎖,每個例項訪問到此物件的同步程式碼塊都會被阻塞。但是如果這時synchronized的引數是this,或者是內部new出來的一個內部非靜態Object,則各個例項擁有不同的鎖,訪問同一個程式碼相同同步塊也是互不干擾。只有例項內部使用了同一個物件鎖才會同步等待。

面試官:那你知道synchronized關鍵字實現同步的原理嗎?

面試者:synchronized在Java虛擬機器中使用監視器鎖來實現。每個物件都有一個監視器鎖,當監視器鎖被佔用時就會處於鎖定狀態。

  執行緒執行一條叫monitorenter的指令來獲取監視器鎖的所有權。如果此監視器鎖的進入數為0,則執行緒進入並將進入數設定為1,成為執行緒所有者。如果執行緒已經擁有該鎖,因為是可重入鎖,可以重新進入,則進入數加1.如果執行緒的監視器鎖被其他執行緒佔用,則阻塞直到此監視器鎖的進入數為0時才能進入該鎖。

  執行緒執行一條叫monitorexit的指令來釋放所有權。執行monitorexit的必須是執行緒的所有者。每次執行此指令,執行緒進入數減1,直到進入數為0。監視器鎖被釋放。

面試官:你剛才提到的可重入鎖是什麼概念,有不可重入鎖嗎?

面試者:我說的可重入鎖是廣義的可重入鎖,當然jdk1.5引入了concurrent包,裡面有Lock介面,它有一個實現叫ReentrantLock。廣義的可重入鎖也叫遞迴鎖,是指同一執行緒外層函式獲得鎖之後,內層還可以再次獲得此鎖。可重入鎖的設計是為了避免死鎖。sun的corba裡的mutex互斥鎖是一種不可重入鎖的實現。自旋鎖也是一種不可重入鎖,本質上是一種忙等鎖,CPU一直迴圈執行"測試並設定"直到可用並取得該鎖,在遞迴的呼叫該鎖時必然會引起死鎖。另外,如果鎖佔用時間較長,自旋鎖會過多的佔用CPU資源,這時使用基於睡眠原理來實現的鎖更加合適。

面試官:你剛才提到了concurrent包,它裡面有哪些鎖的實現?

面試者:常用的有ReentrantLock,它是一種獨佔鎖。ReadWriteLock介面也是一個鎖介面,和Lock介面是一種關聯關係,它返回一個只讀的Lock和只寫的Lock。讀寫分離,在沒有寫鎖的情況下,讀鎖是無阻塞的,提高了執行效率,它是一種共享鎖。ReadWriteLock的實現類為ReentrantReadWriteLock。ReentrantLock和ReentrantReadWriteLock實現都依賴於AbstractQueuedSynchronizer這種抽象佇列同步器。

面試官:鎖還有其他維度的分類嗎?

面試者:還可以分為公平鎖和非公平鎖。非公平鎖是如果一個執行緒嘗試獲取鎖時可以獲取鎖,就直接成功獲取。公平鎖則在鎖被釋放後將鎖分配給等待佇列隊首的執行緒。

面試官:AQS是什麼?

面試者:AQS是一個簡單的框架,這個框架為同步狀態的原子性管理,執行緒的阻塞和非阻塞以及排隊提供了一種通用機制。表現為一個同步器,主要支援獲取鎖和釋放鎖。獲取鎖的時候如果是獨佔鎖就有可能阻塞,如果是共享鎖就有可能失敗。如果是阻塞,執行緒就要進入阻塞佇列,當狀態變成可獲得鎖就修改狀態,已進入阻塞佇列的要從阻塞佇列中移除。釋放鎖時修改狀態位及喚醒其他被阻塞的執行緒。

AQS本質是採用CHL模型完成了一個先進先出的佇列。對於入隊,採用CAS操作,每次比較尾節點是否一致,然後插入到尾節點中。對於出佇列,因為每個節點快取了一個狀態位,不滿足條件時自旋等待,直到滿足條件時將頭節點設定為下一個節點。

面試官:那知道這個佇列的資料結構嗎?

面試者:這個佇列是用一個雙向連結串列實現的。

面試官:你剛才提到AQS是一種通用機制,那它還有哪些應用?

面試者:AQS除了剛才提到的可重入鎖ReentrantLock和ReentrantReadWriteLock之外,還用於不可重入鎖Mutex的實現。java併發包中的同步器如:Semphore,CountDownLatch,FutureTask,CyclicBarrier都是採用這個機制實現的。

旁白:既然問到了併發工具包中的東西,每個都可以引出一堆,但是基本原理已經問出來了,其他的問下去沒什麼意思。轉向下一個問題。

面試官:你黑板上寫的例項是通過new物件建立出來的,還可不可以採用別的方法來建立物件呢?

面試者:還可以使用class類的newInstance方法,Constructor構造器類的newInstance方法,克隆方法和反序列法方法。

面試官:兩種newInstance方法有沒有區別?

面試者:

  ☆ Class類位於java的lang包中,而構造器類是java反射機制的一部分。

  ☆ Class類的newInstance只能觸發無引數的構造方法建立物件,而構造器類的newInstance能觸發有引數或者任意引數的構造方法來建立物件。

  ☆ Class類的newInstance需要其構造方法是共有的或者對呼叫方法可見的,而構造器類的newInstance可以在特定環境下呼叫私有構造方法來建立物件。

  ☆ Class類的newInstance丟擲類建構函式的異常,而構造器類的newInstance包裝了一個InvocationTargetException異常。

  Class類本質上呼叫了反射包構造器類中無引數的newInstance方法,捕獲了InvocationTargetException,將構造器本身的異常丟擲。

面試官:類載入的時候,自己定義了一個類和java自己的類類名和名稱空間都一樣,JVM載入的是哪一個呢?

面試者:呼叫的是java自身的,根據雙親委派模型,最委派Bootstrap的ClassLoader來載入,找不到才去使用Extension的ClassLoader,還找不到才去用Application的ClassLoader,這種機制利於保證JVM的安全。

面試官:你剛才提到的java的反射機制是什麼概念?

面試者:java的反射機制是在執行狀態中,對於任何一個類,都能夠知道它所有的屬性和方法;對於任何一個物件,都能夠呼叫它的任何一個方法和屬性。這種動態的獲取資訊和動態呼叫物件的方法的功能就是java的反射機制。它是jdk動態代理的實現方法。

面試官:java還有沒有其他的動態代理實現?

面試者:還有cglib動態代理。

面試官:這兩種動態代理哪個比較好呢?

面試者:AOP原始碼中同時使用了這兩種動態代理,因為他們各有優劣。jdk動態代理是利用java內部的反射機制來實現,在生成類的過程中比較高效,cglib動態代理則是藉助asm來實現,可以利用asm將生成的類進行快取,所以在類生成之後的相關執行過程中比較高效。但是jdk的動態代理前提是目標類必須基於統一的介面,所以有一定的侷限性。

旁白:面試者都已經提到AOP了,那麼接下來橫向,縱向,怎樣都能問出一大堆問題,就不贅述。基於上面問題,讀者也可以自己畫出一棵知識樹,然後就能找到能對答如流的終極方案:就是基本都沒超過《深入理解java虛擬器》《java併發程式設計實踐》這兩本書,大學學過的《資料結構與演算法》《編譯原理》掌握的好也可以在面試中加分哦。

相關文章