遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

都說金三銀四是找工作的最佳時節,由於本人的個人職業規劃跟目前工作內容不太相符(具體原因就不透露了,領導平時也要來這裡逛,哈哈),四月份挑選了10多家公司投遞簡歷(公司規模從幾十人到上萬人都有),參加了7家公司的電話面試,收穫了5個offer,也還算不錯。下面就分享一下面試過程中一些基礎的,又最常見的問題。不囉嗦了,直接看題。

1. synchronized你用過嗎?synchronized和Lock的區別?synchronized偏向鎖的獲取和撤銷?

這裡著重分析一下第三個問題。對於synchronized這個關鍵字,鄙人剛實習還是一隻菜雞的時候,就聽周圍的大神說,synchronized是一個重量級鎖,開銷很大要少用,本菜只能一臉崇拜(一臉懵逼)的看著他們,不明覺厲。但是本菜也不能一直菜下去是不是,所以也打算對synchronized的原理進行學習,看下大神們為什麼要這樣說。其他的都不管,遇到問題先百度,查看了各種博客,各種資料,其實發現並不像大神們說的那樣子,畢竟JDK團隊也不能忍受世界各地的程序員對他們無休止的吐槽,所以在JDK1.6就對synchronized進行了大量的優化,在JDK1.6之前synchronized的實現統一採用重量級鎖(線程阻塞)來實現的。本菜剛工作的時候就有JDK1.8了,所以周圍的大神對synchronized的認識可能還停留在JDK1.6之前。

還是先補習一下synchronized的基本知識:鎖實際上是加在對象上的,那麼被加了鎖的對象我們稱之為鎖對象,在Java中任何一個對象都能成為鎖對象。很明顯,這裡的重點在於這個對象,先看一下對象在虛擬機中是如何保存的。Java對象在內存中的存儲結構主要有三個部分:對象頭、實例數據和填充部分,用一張圖來說明。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

跟鎖相關的東西是存在Mark Word中,直接來看一下在32位虛擬中Mark Word的存儲內容,看圖。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

先看一下synchronized鎖的幾種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。它會隨著線程競爭情況逐漸升級,但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。

2. 偏向鎖

引入背景:大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖,減少不必要的CAS 操作。偏向鎖,顧名思義,這個鎖會偏向於第一個獲得它的線程,在接下來的執行過程中,假如該鎖沒有被其他線程所獲取,沒有其他線程來競爭該鎖,那麼持有偏向鎖的線程將永遠不需要進行同步操作。如果在運行過程中,遇到了其他線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM 會撤銷它身上的偏向鎖,將鎖升級到輕量級鎖。

偏向鎖的獲取和撤銷過程:

  1. 訪問Mark Word 中偏向鎖的標識是否設置成1,鎖標誌位是否為01,確認為可偏向狀態。
  2. 如果為可偏向狀態,則判斷線程ID是否指向當前線程,如果是進入步驟5,否則進入步驟3。
  3. 如果線程ID並未指向當前線程,則通過CAS 操作競爭鎖。如果競爭成功,則將Mark Word 中線程ID設置為當前線程ID,然後執行步驟5;如果競爭失敗則執行步驟4。
  4. 如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會導致stop the world)。
  5. 執行同步代碼。

偏向鎖是否開啟可以使用JVM的參數來控制:

  • 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 關閉偏向鎖:-XX:-UseBiasedLocking

文字描述太過抽象,還是畫一圖個來理解。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

3. 輕量級鎖

輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖。

輕量級鎖的加鎖過程:在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態且不允許進行偏向(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(LockRecord)的空間,用於存儲鎖對象目前的Mark Word 的拷貝,官方稱之為Displaced Mark Word。拷貝成功後,虛擬機將使用CAS 操作嘗試將對象的Mark Word 更新為指向LockRecord 的指針,並將Lock Record裡的owner 指針指向object mark word。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word 的鎖標誌位設置為“00”,即表示此對象處於輕量級鎖定狀態。如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word 是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,當競爭線程嘗試佔用輕量級鎖失敗多次之後,輕量級鎖就會膨脹為重量級鎖,重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖後喚醒他。鎖標誌的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

自旋次數可以通過虛擬機參數-XX:PreBlockSpin來進行更改,默認為10。

上述流程還是畫一個圖來理解。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

4. 重量級鎖

重量級鎖大家都比較瞭解,重量級鎖是依賴對象內部的monitor鎖來實現的,而monitor又依賴操作系統的MutexLock(互斥鎖)來實現的,所以重量級鎖也被成為互斥鎖。所以大神們說synchronized開銷大,針對的是重量級鎖而言。主要原因是升級到重量級鎖之後,會把等待想要獲得鎖的線程進行阻塞,被阻塞的線程不會消耗cup。但是阻塞或者喚醒一個線程時,都需要操作系統來幫忙,進行狀態轉換。狀態轉換是需要消耗很多時間的,有可能比用戶執行代碼的時間還要長。還是畫一個經典的圖來理解。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

我們把幾種鎖進行一個比較,直接上圖。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

當然面試時候還有一些其他的問題也非常常見,比如說synchronized是樂觀鎖還是悲觀鎖?樂觀鎖一定比悲觀鎖好嗎?使用CAS機制的3大問題?請談談AQS是怎麼回事兒?ReentrantLock是如何實現可重入性的?如何讓Java的線程彼此同步?你瞭解過哪些同步器?用過線程池嗎,介紹一下線程池的各個構造參數以及線程池的運行原理?請談談 volatile 有什麼作用,以及volatile和synchronized的區別?你用過ThreadlLocal嗎,請談談你的使用場景以及原理?什麼是不可變對象,它對寫併發有什麼幫助?

5. Redis的持久化

Redis的數據全部在內存中,如果突然宕機,數據就會全部丟失,因此必須有一種機制來保證Redis的數據在遇到突發狀況的時候不會丟失,或者只丟失少量,於是必須根據一些策略來把Redis內存中的數據寫到磁盤中,這樣當Redis服務重啟中,就可以根據磁盤中的數據來恢復數據到內存中。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

Redis的持久化機制:AOF、RDB以及混合持久化(4.0版本以後支持,後續的Redis專題詳細介紹)。

①. RDB

RDB(快照)持久化:保存某個時間點的全量數據快照。

RDB是一次的全量備份,即週期性的把Redis當前內存中的全量數據寫入到一個快照文件中。Redis是單線程程序,這個線程要同時負責多個客戶端的讀寫請求,還要負責週期性的把當前內存中的數據寫到快照文件中RDB中,數據寫到RDB文件是IO操作,IO操作會嚴重影響Redis的性能,甚至在持久化的過程中,讀寫請求會阻塞,為了解決這些問題,Redis需要同時進行讀寫請求和持久化操作,這樣又會導致另外的問題,持久化的過程中,內存中的數據還在改變,假如Redis正在進行持久化一個大的數據結構,在這個過程中客戶端發送一個刪除請求,把這個大的數據結構刪掉了,這時候持久化的動作還沒有完成,那麼Redis該怎麼辦呢?

Redis使用操作系統的多進程寫時複製機制(Copy On Write)機制來實現快照的持久化,在持久化過程中調用glibc(Linux下的C函數庫)的函數fork()產生一個子進程,快照持久化完全交給子進程來處理,父進程繼續處理客戶端的讀寫請求。子進程剛剛產生時,和父進程共享內存裡面的代碼段和數據段,這是Linux操作系統的機制,為了節約內存資源,所以儘可能讓父子進程共享內存,這樣在進程分離的一瞬間,內存的增長几乎沒有明顯變化。

簡單介紹一下寫時複製和fork。

fork:fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個進程也可以做不同的事。一個進程調用fork()函數後,系統先給新的進程分配資源,例如存儲數據和代碼的空間。然後把原來的進程的所有值都複製到新的新進程中,只有少數值與原來的進程的值不同,相當於克隆了一個自己。

寫時複製(Copy On Write):

資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在Linux程序中,fork()會產生一個和父進程完全相同的子進程,子進程在此後會調用exec()開始執行。

所以對於上面的那個問題,處理方式是子進程對當前內存中的數據進行持久化,並不會修改當前的數據結構,如果父進程收到了讀寫請求,那麼會把處理的那一部分數據複製一份到內存,對複製後的數據進行修改,所以即使對某個數據進行了修改,Redis持久化到RDB中的數據也是未修改的數據,這也是把RDB文件稱為"快照"文件的原因,子進程所看到的數據在它被創建的一瞬間就固定下來了,父進程修改的某個數據只是該數據的複製品。這裡再深入一點,Redis內存中的全量數據由一個個的"數據段頁面"組成,每個數據段頁面的大小為4K,客戶端要修改的數據在哪個頁面中,就會複製一份這個頁面到內存中,這個複製的過程稱為"頁面分離",在持久化過程中,隨著分離出的頁面越來越多,內存就會持續增長,但是不會超過原內存的2倍,因為在一次持久化的過程中,幾乎不會出現所有的頁面都會分離的情況,讀寫請求針對的只是原數據中的小部分,大部分Redis數據還是"冷數據"。用一個圖來表示。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

對RDB快照持久化過程做一個總結:

  • Redis使用fork函數複製一份當前進程的副本(子進程)。
  • 父進程繼續接收並處理客戶端發來的命令,而子進程開始將內存中的數據寫入硬盤中的臨時文件。
  • 當子進程寫入完所有數據後會用該臨時文件替換舊的RDB文件,至此,一次快照操作完成。

注意:Redis在進行快照的過程中不會修改RDB文件,只有快照結束後才會將舊的文件替換成新的,也就是說任何時候RDB文件都是完整的。 這就使得我們可以通過定時備份RDB文件來實現Redis數據庫的備份, RDB文件是經過壓縮的二進制文件,佔用的空間會小於內存中的數據,更加利於傳輸。

RDB快照產生方式:

手動觸發

  • SAVE:阻塞Redis的服務器進程,知道RDB文件被創建完畢。
  • BGSAVE:fork出一個子進程來創建RDB文件,不阻塞服務器進程。lastsave指令可以查看最近的備份時間。

自動觸發

  • 根據Redis.conf配置裡的save m n定時觸發(用的是BGSAVE)
遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

  • 主從複製時,主節點自動觸發
  • 執行Debug Relaod
  • 執行Shutdown且沒有開啟AOF持久化

瞭解了以上內容,我們可以畫一個圖來表示RDB快照持久化的流程。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

②. AOF

AOF日誌存儲的是Redis服務器的順序指令序列,即對內存中數據進行修改的指令記錄。當Redis收到客戶端修改指令後,先進行參數校驗,如果校驗通過,先把該指令存儲到AOF日誌文件中,也就是先存到磁盤,然後再執行該修改指令。當Redis宕機後重啟後,可以讀取該AOF文件中的指令,進行數據恢復,恢復的過程就是把記錄的指令再順序執行一次,這樣就可以恢復到宕機之前的狀態。用一個圖來表示AOF的過程。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

Redis在長期運行過程中,AOF日誌會越來越大,如果Redis服務重啟後根據很大的AOF文件來順序執行指令,將會非常耗時,導致Redis服務長時間無法對外提供服務,所以需要對AOF文件進行"減肥"。"減肥"的過程稱作AOF重寫(rewrite)。AOF Rewrite 的原理是,主進程fork一個子進程,對當前內存中的數據進行遍歷,轉換成一系列的Redis操作指令,並序列化到一個新的AOF日誌中,然後把序列化操作期間新收到的操作指令追加到新的AOF文件中,追加完畢後就立即替換舊的AOF文件,這樣就完成了"減肥"工作。Redis把操作指令追加到AOF文件這個過程,並不是直接寫到AOF文件中,而是先寫到操作系統的內存緩存中,這個內存緩存是由操作系統內核分配的,然後操作系統內核會異步地把內存緩存中的Redis操作指令刷寫到AOF文件中。用一個圖來表示。

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

AOF相關參數配置:

遲來的四月java面經分享,七面收割五個offer,大廠也不過如此!

簡單對重寫的幾個參數做一個說明:比如說上一次AOF rewrite之後,是128mb然後就會接著128mb繼續寫AOF的日誌,如果發現增長的比例,超過了之前的100%,256mb,就可能會去觸發一次rewrite

但是此時還要去跟min-size,64mb去比較,256mb > 64mb,才會去觸發rewrite。

我們對Redis持久化機制做一個對比:

RDB的優缺點

優點:

  • RDB會生成多個數據文件,每個數據文件都代表了某一個時刻中Redis的數據,這種多個數據文件的方式,非常適合做冷備,可以將這種完整的數據文件發送到一些遠程的安全存儲上去。
  • 當進行RDB持久化時,對Redis服務處理讀寫請求的影響非常小,可以讓Redis保持高性能,因為Redis主進程只需要fork一個子進程,讓子進程執行磁盤IO操作來進行RDB持久化即可。生成一次RDB文件的過程就是把當前時刻內存中的數據一次性寫入文件中,而AOF則需要先把當前內存中的小量數據轉換為操作指令,然後把指令寫到內存緩存中,然後再刷寫入磁盤。
  • 相對於AOF持久化機制來說,直接基於RDB數據文件來重啟和恢復Redis的數據會更加快速。AOF,存放的是指令日誌,做數據恢復的時候,要回放和執行所有的指令日誌,從而恢復內存中的
    所有數據。而RDB,就是一份數據文件,恢復的時候,直接加載到內存中即可。

缺點:

  • 如果想要在Redis故障時,儘可能少的丟失數據,那麼RDB沒有AOF好。一般來說,RDB數據快照文件,都是每隔5分鐘,或者更長時間生成一次,這個時候就得接受一旦Redis進程宕機,那麼會丟失最近5分鐘的數據。這個問題,也是RDB最大的缺點,就是不適合做第一優先的恢復方案,如果你依賴RDB做第一優先恢復方案,會導致數據丟失的比較多。
  • RDB每次在fork子進程來執行RDB快照數據文件生成的時候,如果數據文件特別大,可能會導致對客戶端提供的服務暫停數毫秒,甚至數秒。所以一般不要讓生成RDB文件的間隔太長,否則每次生成的RDB文件太大了,對Redis本身的性能會有影響。

AOF的優缺點

優點:

  • AOF可以更好的保護數據不丟失,一般AOF會每隔1秒,通過一個後臺線程執行一次fsync操作,最多丟失1秒鐘的數據。
  • AOF日誌文件以append-only模式寫入,所以沒有任何磁盤尋址的開銷,寫入性能非常高,而且文件不容易破損,即使文件尾部破損,也很容易修復。
  • AOF日誌文件即使過大的時候,出現後臺重寫操作,也不會影響客戶端的讀寫。因為在rewrite的時候,會對其中的指令進行壓縮,會創建出一份需要恢復數據的最小日誌出來。
  • AOF日誌文件的命令通過非常可讀的方式進行記錄,這個特性非常適合做災難性的誤刪除的緊急恢復。比如某人不小心用flushall命令清空了所有數據,只要這個時候後臺rewrite還沒有發生,那麼就可以立即拷貝AOF文件,將最後一條flushall命令給刪了,然後再將該AOF文件放回去,就可以通過恢復機制,自動恢復所有數據。

缺點:

  • 對於同一份數據來說,AOF日誌文件通常比RDB數據快照文件更大。
  • AOF的寫性能比RDB的寫性能低,因為AOF一般會配置成每秒fsync一次日誌文件,當然,每秒一次fsync,性能也還是很高的,只不過比起RDB來說性能低,如果要保證一條數據都不丟,也是可以的,AOF的fsync設置成每寫入一條數據,fsync一次,但是這樣,Redis的性能會大大下降。
  • 基於AOF文件做恢復的速度不如基於RDB文件做恢復的速度。

那麼我們在實際項目中該怎麼選擇Redis的持久化方案呢?這裡簡單的給出我個人的建議:

  • 不要僅僅使用RDB,因為那樣會導致你丟失很多數據
  • 也不要僅僅使用AOF,一是數據恢復慢,二是可靠性也不如RDB,畢竟RDB文件中存儲的就是某一時刻實實在在的數據,而AOF只是操作指令,把數據轉換為操作指令不一定是百分百沒問題的。
  • 綜合使用AOF和RDB兩種持久化機制,用AOF來保證數據不丟失,作為數據恢復的第一選擇; 用RDB來做不同程度的冷備,在AOF文件都丟失或損壞不可用的時候,還可以使用RDB來進行快速的數據恢復。

關於Redis還有很多常見的面試題,比如說Redis單線程為什麼還如此快?Redis的數據類型以及使用場景?Redis的高可用方案?你說你用了哨兵集群,那麼談談腦裂場景怎麼解決?Redis怎麼實現分佈式鎖以及會遇到的問題,你這麼解決這些問題?Redis的緩存擊穿、緩存穿透、緩存雪崩是什麼,怎麼解決?Redis和DB數據一致性問題解決方案?Redis支持事務嗎,具體是怎麼樣的?這些知識點會在後續的Redis專題中介紹。

最後再分享一下在面試中的一些心得體會。一般開場都會讓自我介紹,自我介紹的時候一定要流暢,可以事先練習,千萬不要結結巴巴。把最熟悉的知識點寫在最前面,面試官一般會按照你簡歷上寫的順序去問。比如你把多線程寫在最前面,一般都會聊到synchronized、Lock以及多線程在項目中實際的運用(這個一定要準備),既然都問到了鎖,那麼分佈式鎖肯定會引申出來。比如你回答了分佈式鎖是使用Redis實現的,既然扯到了Redis上面,上述我列出的這些問題大概率會出現。Redis都聊了,那麼不聊一下關係型數據庫好像不太好吧,如果你簡歷上寫了熟悉MySQL,那麼來聊聊MySQL。關於MySQL,肯定就會問到MySQL的存儲引擎。嗯,你說你用的是InnoDB、MyISAM引擎,那麼就聊聊這2個引擎區別。區別說完了,那你再說一下InnoDB引擎的索引以及聚集索引和非聚集索引,你把Hash、B+Tree的原理給說清楚,接下來肯定就是問你在項目中對MySQL的實際優化經驗,那麼慢查詢、執行計劃分析、表創建的技巧、索引的創建技巧、SQL編寫的技巧肯定會隨之而來。既然聊到了優化,那麼接下來就再說說JVM相關的知識,從內存模型到垃圾回收算法、垃圾回收器、類加載機制、內存洩漏等,最後再到問你線上環境有過實際的調優經驗嗎,怎麼實現的?這些都回答完了,可能還會來一個開放性的問題,比如說一個生產環境,上線半個小時,Full GC發生了上百次,而Minor GC只發生了幾次,請你分析下可能是什麼原因造成?再或者說,一個生產環境,沒有報OOM,但是用戶線程也沒有執行了,直觀現象就是應用沒有日誌輸出,請你分析下可能是什麼原因造成的?這些都是面試過程中Java基礎部分最常見的問題,這些聊完了後面就會涉及到框架、項目相關的問題。在自己擅長的部分儘量多聊一會兒,不要被面試官牽著鼻子走,畢竟面試時間就那麼長,這樣就儘可能的揚長避短。在面試過程中,儘量把沒有答上的問題記下來,面試完了做一個總結,看是因為什麼原因沒答上來,是的確不知道還是因為表達不清晰還是過於緊張。多參加幾次面試後會發現,基礎部分的問題其實都大同小異,情緒也不會那麼緊張,此時收割offer的概率那就大大地提高咯。

點關注,不迷路!持續更新Java技術的研究與分享

推薦閱讀:

2020還不會性能調優?啃下這20W字Java性能調優筆記,隨便漲薪13K!

應該是史上最全SpringCloud微服務筆記,掌握已超過80%Java面試者


分享到:


相關文章: