回復(fù)“000”獲取程序員必備電子書 大家好,我是老田,,今天給大家分享的是一位兩年多工作經(jīng)驗的小伙伴面試經(jīng)歷,,恭喜他成功上岸,收到了offer,!本文大部分內(nèi)容是這位朋友所寫,,我對一小部分內(nèi)容進(jìn)行修正和調(diào)整,話不多說,,咱們直入主題,。 面試官到了后,看著簡歷,,然后來一句千年不變的: 1,、說說你對HashMap的理解
關(guān)于這道題,,我們可以從幾個方面去回答: 數(shù)據(jù)結(jié)構(gòu)JDK1.7之前采用的是數(shù)組+鏈表,。 JDK1.8后采用的是數(shù)組+鏈表(紅黑樹),當(dāng)鏈表的長度大于8,,并且數(shù)組長度為64時,,如果再往此鏈表上添加數(shù)據(jù),,那么該鏈表就會轉(zhuǎn)為紅黑樹,。 put方法過程看流程圖,這樣印象更深刻: 線程安全問題在多線程環(huán)境下,,1.7 會產(chǎn)生死循環(huán),、數(shù)據(jù)丟失、數(shù)據(jù)覆蓋的問題,1.8 中會有數(shù)據(jù)覆蓋的問題,,以 1.8 為例,,當(dāng) A 線程判斷 index 位置為空后正好掛起,B 線程開始往 index 位置的寫入節(jié)點(diǎn)數(shù)據(jù),,這時 A 線程恢復(fù)現(xiàn)場,,執(zhí)行賦值操作,就把 A 線程的數(shù)據(jù)給覆蓋了,;還有++size 這個地方也會造成多線程同時擴(kuò)容等問題,。 2、hash沖突解決方案有哪些
一共有四種方法: 1,、再哈希法:如果hash出的index已經(jīng)有值,就再hash,,不行繼續(xù)hash,,直至找到空的index位置,要相信瞎貓總能碰上死耗子,。這個辦法最容易想到,。但有2個缺點(diǎn):
2,、開放地址方法:如果hash出的index已經(jīng)有值,,通過算法在它前面或后面的若干位置尋找空位,這個和再hash算法差別不大,。 3,、建立公共溢出區(qū): 把沖突的hash值放到另外一塊溢出區(qū)。 4、鏈?zhǔn)降刂贩ǎ?/strong> 把產(chǎn)生hash沖突的hash值以鏈表形式存儲在index位置上,。HashMap用的就是該方法,。優(yōu)點(diǎn)是不需要另外開辟新空間,也不會丟失數(shù)據(jù),,尋址也比較簡單,。但是隨著hash鏈越來越長,尋址也是更加耗時,。好的hash算法就是要讓鏈盡量短,,最好一個index上只有一個值。也就是盡可能地保證散列地址分布均勻,,同時要計算簡單,。 3、說說你對Spring IOC的理解
IOC就是控制反轉(zhuǎn),,是指創(chuàng)建對象的控制權(quán)的轉(zhuǎn)移。以前創(chuàng)建對象的主動權(quán)和時機(jī)是由自己把控的,,而現(xiàn)在這種權(quán)力轉(zhuǎn)移到Spring容器中,,并由容器根據(jù)配置文件去創(chuàng)建實(shí)例和管理各個實(shí)例之間的依賴關(guān)系。對象與對象之間松散耦合,,也利于功能的復(fù)用,。DI依賴注入,和控制反轉(zhuǎn)是同一個概念的不同角度的描述,,即 應(yīng)用程序在運(yùn)行時依賴IoC容器來動態(tài)注入對象需要的外部資源,。 最直觀的表達(dá)就是,IOC讓對象的創(chuàng)建不用去new了,,可以由spring自動生產(chǎn),,使用java的反射機(jī)制,根據(jù)配置文件在運(yùn)行時動態(tài)的去創(chuàng)建對象以及管理對象,,并調(diào)用對象的方法的,。 Spring的IOC有三種注入方式 :構(gòu)造器注入、setter方法注入,、根據(jù)注解注入,。
4,、Spring AOP在工作中有用過嗎?
有用過,。 AOP(Aspect-Oriented Programming,面向切面編程)能夠?qū)⒛切┡c業(yè)務(wù)無關(guān),,卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任(例如事務(wù)處理,、日志管理、權(quán)限控制等)封裝起來,,便于減少系統(tǒng)的重復(fù)代碼,,降低模塊間的耦合度,并有利于未來的可擴(kuò)展性和可維護(hù)性,。 Spring AOP是基于動態(tài)代理的,,如果要代理的對象實(shí)現(xiàn)了某個接口,那么Spring AOP就會使用JDK動態(tài)代理去創(chuàng)建代理對象,;而對于沒有實(shí)現(xiàn)接口的對象,,就無法使用JDK動態(tài)代理,轉(zhuǎn)而使用CGlib動態(tài)代理生成一個被代理對象的子類來作為代理,。 當(dāng)然也可以使用AspectJ,,Spring AOP中已經(jīng)集成了AspectJ,AspectJ應(yīng)該算得上是Java生態(tài)系統(tǒng)中最完整的AOP框架了,。使用AOP之后我們可以把一些通用功能抽象出來,,在需要用到的地方直接使用即可,這樣可以大大簡化代碼量,。我們需要增加新功能也方便,,提高了系統(tǒng)的擴(kuò)展性。日志功能,、事務(wù)管理和權(quán)限管理等場景都用到了AOP,。 5、@Controller和@RestController有什么區(qū)別,?
對比源碼可知
6、熟悉Bean的生命周期嗎,?
首先說一下Servlet的生命周期:實(shí)例化,,初始init,,接收請求service,銷毀destroy,; Spring上下文中的Bean生命周期也類似,,如下: (1)實(shí)例化Bean: 對于BeanFactory容器,當(dāng)客戶向容器請求一個尚未初始化的bean時,,或初始化bean的時候需要注入另一個尚未初始化的依賴時,,容器就會調(diào)用createBean進(jìn)行實(shí)例化。對于ApplicationContext容器,,當(dāng)容器啟動結(jié)束后,,通過獲取BeanDefinition對象中的信息,實(shí)例化所有的bean,。 (2)設(shè)置對象屬性(依賴注入): 實(shí)例化后的對象被封裝在BeanWrapper對象中,,緊接著,Spring根據(jù)BeanDefinition中的信息 以及 通過BeanWrapper提供的設(shè)置屬性的接口完成依賴注入,。 (3)處理Aware接口: 接著,,Spring會檢測該對象是否實(shí)現(xiàn)了xxxAware接口,并將相關(guān)的xxxAware實(shí)例注入給Bean: ①如果這個Bean已經(jīng)實(shí)現(xiàn)了BeanNameAware接口,,會調(diào)用它實(shí)現(xiàn)的setBeanName(String beanId)方法,,此處傳遞的就是Spring配置文件中Bean的id值; ②如果這個Bean已經(jīng)實(shí)現(xiàn)了BeanFactoryAware接口,,會調(diào)用它實(shí)現(xiàn)的setBeanFactory()方法,,傳遞的是Spring工廠自身。 ③如果這個Bean已經(jīng)實(shí)現(xiàn)了ApplicationContextAware接口,,會調(diào)用setApplicationContext(ApplicationContext)方法,,傳入Spring上下文; (4)BeanPostProcessor: 如果想對Bean進(jìn)行一些自定義的處理,,那么可以讓Bean實(shí)現(xiàn)了BeanPostProcessor接口,,那將會調(diào)用postProcessBeforeInitialization(Object obj, String s)方法,。 (5)InitializingBean 與 init-method: 如果Bean在Spring配置文件中配置了 init-method 屬性,則會自動調(diào)用其配置的初始化方法,。 (6)如果這個Bean實(shí)現(xiàn)了BeanPostProcessor接口,,將會調(diào)用postProcessAfterInitialization(Object obj, String s)方法;由于這個方法是在Bean初始化結(jié)束時調(diào)用的,,所以可以被應(yīng)用于內(nèi)存或緩存技術(shù);
(7)DisposableBean: 當(dāng)Bean不再需要時,,會經(jīng)過清理階段,,如果Bean實(shí)現(xiàn)了DisposableBean這個接口,會調(diào)用其實(shí)現(xiàn)的destroy()方法,; (8)destroy-method: 最后,,如果這個Bean的Spring配置中配置了destroy-method屬性,會自動調(diào)用其配置的銷毀方法,。 7,、說說Synchronized和ReentrantLock的區(qū)別
相似點(diǎn)這兩種同步方式有很多相似之處,,它們都是加鎖方式同步,,而且都是阻塞式的同步,也就是說當(dāng)如果一個線程獲得了對象鎖,,進(jìn)入了同步塊,,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進(jìn)行線程阻塞和喚醒的代價是比較高的. 區(qū)別這兩種方式最大區(qū)別就是對于Synchronized來說,,它是java語言的關(guān)鍵字,,是原生語法層面的互斥,需要jvm實(shí)現(xiàn),。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,,需要lock()和unlock()方法配合try/finally語句塊來完成。 synchronized經(jīng)過編譯,,會在同步塊的前后分別形成monitorenter和monitorexit這個兩個字節(jié)碼指令,。在執(zhí)行monitorenter指令時,首先要嘗試獲取對象鎖,。如果這個對象沒被鎖定,,或者當(dāng)前線程已經(jīng)擁有了那個對象鎖,,把鎖的計算器加1,,相應(yīng)的,,在執(zhí)行monitorexit指令時會將鎖計算器就減1,當(dāng)計算器為0時,,鎖就被釋放了,。如果獲取對象鎖失敗,,那當(dāng)前線程就要阻塞,直到對象鎖被另一個線程釋放為止,。 由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,,相比Synchronized,ReentrantLock類提供了一些高級功能,,主要有以下3項: 1.等待可中斷,,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,,這相當(dāng)于Synchronized來說可以避免出現(xiàn)死鎖的情況,。 2.公平鎖,多個線程等待同一個鎖時,,必須按照申請鎖的時間順序獲得鎖,,Synchronized鎖非公平鎖,ReentrantLock默認(rèn)的構(gòu)造函數(shù)是創(chuàng)建的非公平鎖,,可以通過參數(shù)true設(shè)為公平鎖,,但公平鎖表現(xiàn)的性能不是很好。 3.鎖綁定多個條件,,一個ReentrantLock對象可以同時綁定對個對象,。 8、了解volatile關(guān)鍵字嗎,?
一旦一個共享變量(類的成員變量,、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
volatile標(biāo)記的變量不會被編譯器優(yōu)化,;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。 9,、說說你對并發(fā)編程中CAS的理解
CAS叫做CompareAndSwap,,比較并交換,主要是通過處理器的指令來保證操作的原子性,,它包含三個操作數(shù):
當(dāng)執(zhí)行CAS指令時,,只有當(dāng)V等于A時,才會用B去更新V的值,,否則就不會執(zhí)行更新操作,。 CAS的缺點(diǎn)主要有3點(diǎn)ABA問題:ABA的問題指的是在CAS更新的過程中,當(dāng)讀取到的值是A,,然后準(zhǔn)備賦值的時候仍然是A,,但是實(shí)際上有可能A的值被改成了B,然后又被改回了A,,這個CAS更新的漏洞就叫做ABA,。只是ABA的問題大部分場景下都不影響并發(fā)的最終效果。 Java中有 循環(huán)時間長開銷大:自旋CAS的方式如果長時間不成功,,會給CPU帶來很大的開銷。 只能保證一個共享變量的原子操作:只對一個共享變量操作可以保證原子性,,但是多個則不行,,多個可以通過 9,、線程有哪些狀態(tài),?
Java中線程的狀態(tài)分為6種,。
想獲取面試官的青睞,,還是得說說線程的狀態(tài)流轉(zhuǎn),可以根據(jù)下面這張圖來描述: 10,、有用過線程池嗎,?是怎么用的?
有用過,, 創(chuàng)建線程有兩種方式
使用ThreadPoolExecutor是JDK原生態(tài)創(chuàng)建線程池,,也可以使用Executors工具類來創(chuàng)建線程池,并Executors大多數(shù)都是基于ThreadPoolExecutor進(jìn)行二次封裝,。 以下是Executors方式創(chuàng)建線程池的幾種方式:
通常不建議使用Executors來創(chuàng)建線程池,,因為該方式中很多參數(shù)都已經(jīng)給你設(shè)置好了,所以在使用的時候,,如果使用不當(dāng)或者對參數(shù)沒有認(rèn)證考察可能會產(chǎn)生很多意想不到的問題:比如隊列多大,,造成 11、說說線程池中那幾個核心參數(shù)和含義
corePoolSize:核心線程數(shù)線程池維護(hù)的最小線程數(shù)量,,核心線程創(chuàng)建后不會被回收(注意:設(shè)置allowCoreThreadTimeout=true后,,空閑的核心線程超過存活時間也會被回收)。 大于核心線程數(shù)的線程,在空閑時間超過keepAliveTime后會被回收,。 線程池剛創(chuàng)建時,,里面沒有一個線程,當(dāng)調(diào)用 execute() 方法添加一個任務(wù)時,,如果正在運(yùn)行的線程數(shù)量小于 corePoolSize,,則馬上創(chuàng)建新線程并運(yùn)行這個任務(wù)。 maximumPoolSize:最大線程數(shù)線程池允許創(chuàng)建的最大線程數(shù)量,。 當(dāng)添加一個任務(wù)時,,核心線程數(shù)已滿,線程池還沒達(dá)到最大線程數(shù),,并且沒有空閑線程,,工作隊列已滿的情況下,創(chuàng)建一個新線程,,然后從工作隊列的頭部取出一個任務(wù)交由新線程來處理,,而將剛提交的任務(wù)放入工作隊列尾部。 keepAliveTime:空閑線程存活時間當(dāng)一個可被回收的線程的空閑時間大于keepAliveTime,,就會被回收,。 可被回收的線程:
unit:時間單位keepAliveTime的時間單位: TimeUnit.NANOSECONDS workQueue:工作隊列新任務(wù)被提交后,,會先添加到工作隊列,任務(wù)調(diào)度時再從隊列中取出任務(wù),。工作隊列實(shí)現(xiàn)了BlockingQueue接口,。 JDK默認(rèn)的工作隊列有五種:
threadFactory:線程工廠創(chuàng)建線程的工廠,,可以設(shè)定線程名,、線程編號等。 12,、有了解過JVM嗎,?
JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫 ,,JVM在執(zhí)行Java程序時,,會把它管理的內(nèi)存劃分為若干個的區(qū)域,每個區(qū)域都有自己的用途和創(chuàng)建銷毀時間,。如下圖所示,,可以分為兩大部分,線程私有區(qū)和共享區(qū),。 下圖是根據(jù)自己理解畫的一個JVM內(nèi)存模型架構(gòu)圖: JVM內(nèi)存分為線程私有區(qū)和線程共享區(qū),。 線程私有區(qū) 1、程序計數(shù)器 當(dāng)同時進(jìn)行的線程數(shù)超過CPU數(shù)或其內(nèi)核數(shù)時,,就要通過時間片輪詢分派CPU的時間資源,,不免發(fā)生線程切換,。這時,,每個線程就需要一個屬于自己的計數(shù)器來記錄下一條要運(yùn)行的指令。如果執(zhí)行的是JAVA方法,,計數(shù)器記錄正在執(zhí)行的java字節(jié)碼地址,,如果執(zhí)行的是native方法,則計數(shù)器為空,。 2,、虛擬機(jī)棧 線程私有的,與線程在同一時間創(chuàng)建,。管理JAVA方法執(zhí)行的內(nèi)存模型,。每個方法執(zhí)行時都會創(chuàng)建一個楨棧來存儲方法的的變量表,、操作數(shù)棧、動態(tài)鏈接方法,、返回值,、返回地址等信息。棧的大小決定了方法調(diào)用的可達(dá)深度(遞歸多少層次,,或嵌套調(diào)用多少層其他方法,, 如果請求的棧深度大于最大可用深度,則拋出 如果棧是可動態(tài)擴(kuò)展的,,但沒有內(nèi)存空間支持?jǐn)U展,則拋出 使用jclasslib工具可以查看class類文件的結(jié)構(gòu),。下圖為棧幀結(jié)構(gòu)圖: 一個線程對應(yīng)一個虛擬機(jī)棧,一個虛擬機(jī)棧對應(yīng)多個棧幀,,每個棧幀的的入棧和出棧表示一個方法的調(diào)用,。 3、本地方法棧 與虛擬機(jī)棧作用相似,。但它不是為Java方法服務(wù)的,,而是本地方法(C語言)。由于規(guī)范對這塊沒有強(qiáng)制要求,,不同虛擬機(jī)實(shí)現(xiàn)方法不同,。 線程共享區(qū) 1、方法區(qū) 線程共享的,,用于存放被虛擬機(jī)加載的類的元數(shù)據(jù)信息,,如常量、靜態(tài)變量和即時編譯器編譯后的代碼,。若要分代,,算是永久代(老年代),以前類大多“static”的,,很少被卸載或收集,,現(xiàn)回收廢棄常量和無用的類。其中運(yùn)行時常量池存放編譯生成的各種常量,。(如果hotspot虛擬機(jī)確定一個類的定義信息不會被使用,,也會將其回收?;厥盏幕緱l件至少有:所有該類的實(shí)例被回收,,而且裝載該類的ClassLoader被回收),。 2、堆 存放對象實(shí)例和數(shù)組,,是垃圾回收的主要區(qū)域,,分為新生代和老年代。剛創(chuàng)建的對象在新生代的Eden區(qū)中,,經(jīng)過GC后進(jìn)入新生代的S0區(qū)中,,再經(jīng)過GC進(jìn)入新生代的S1區(qū)中,15次GC后仍存在就進(jìn)入老年代,。這是按照一種回收機(jī)制進(jìn)行劃分的,,不是固定的。若堆的空間不夠?qū)嵗峙?,則 13、類加載機(jī)制是什么,?
JVM類加載分為5個過程:加載,,驗證,,準(zhǔn)備,解析,,初始化,,使用,卸載,,如下圖所示: 下面來看看加載,,驗證,準(zhǔn)備,,解析,,初始化這5個過程的具體動作。 加載 加載主要是將.class文件(并不一定是.class,??梢允荶IP包,網(wǎng)絡(luò)中獲?。┲械亩M(jìn)制字節(jié)流讀入到JVM中。在加載階段,,JVM需要完成3件事:1)通過類的全限定名獲取該類的二進(jìn)制字節(jié)流,;2)將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu),;3)在內(nèi)存中生成一個該類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口,。 連接 驗證 驗證是連接階段的第一步,,主要確保加載進(jìn)來的字節(jié)流符合JVM規(guī)范。驗證階段會完成以下4個階段的檢驗動作:1)文件格式驗證 2)元數(shù)據(jù)驗證(是否符合Java語言規(guī)范) 3)字節(jié)碼驗證(確定程序語義合法,,符合邏輯) 4)符號引用驗證(確保下一步的解析能正常執(zhí)行) 準(zhǔn)備 主要為靜態(tài)變量在方法區(qū)分配內(nèi)存,,并設(shè)置默認(rèn)初始值。 解析 是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程,。 初始化 初始化階段是類加載過程的最后一步,,主要是根據(jù)程序中的賦值語句主動為類變量賦值。注:1)當(dāng)有父類且父類為初始化的時候,,先去初始化父類,;2)再進(jìn)行子類初始化語句。 14,、垃圾回收算法有哪些,?
GC最基礎(chǔ)的算法有三種:標(biāo)記 -清除算法,、復(fù)制算法、標(biāo)記-壓縮算法,。 我們常用的垃圾回收器一般都采用分代收集算法,,然后針對不同的代進(jìn)行使用不同的算法。
15,、熟悉哪些JVM調(diào)優(yōu)參數(shù),?
「堆棧內(nèi)存相關(guān)」
「垃圾收集器相關(guān)」
「輔助信息相關(guān)」
16,、熟悉分布式鎖嗎,?有哪些實(shí)現(xiàn)方案?
項目中有用到分布式鎖,,使用 分布式鎖實(shí)現(xiàn)方案,常見有如下幾種:
17,、哪一種方案是最好的,?
1、 2、ZK鎖具備高可用,、可重入,、阻塞鎖特性,可解決失效死鎖問題,。但是因為需要頻繁的創(chuàng)建和刪除節(jié)點(diǎn),,性能上不如Redis方式。 3,、 4,、數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖,,對數(shù)據(jù)庫表侵入較大,每個表需要增加version等字段,,高并發(fā)下存在很多更新失敗,。數(shù)據(jù)庫寫入是磁盤io,性能方面差一些,。數(shù)據(jù)庫能支持的最大 總結(jié)小伙伴本次面試中發(fā)揮的還是挺好的,,最終收獲offer,,恭喜這位朋友。 最后,,希望大家平時就算不面試,,也要為日后的面試做準(zhǔn)備,做一個能進(jìn)能退的人,。
|
|