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