(高頻問題)21-40 計算機 Java后端 實習(xí) and 秋招 面試高頻問題匯總
21.怎么判斷用戶是否登錄、如果有多臺服務(wù)器,怎么判斷一個請求是否已經(jīng)登錄
怎么判斷用戶是否登錄:一種常見的方法是使用cookie和session機制,原理就是用戶登錄的時候在服務(wù)器端創(chuàng)建一個session,登錄成功后,把用戶信息放在session里,接下里,服務(wù)器會把sessionID返回給用戶的瀏覽器,瀏覽器接收到這個cookie后,用戶再訪問網(wǎng)站的URL地址時,瀏覽器會順帶著把這個網(wǎng)站下的cookies全部發(fā)送給服務(wù)器,服務(wù)器檢查cookies里有木有sessionID,如果有,會根據(jù)sessionID找到session,然后再判斷session里有木有用戶信息,有則用戶已登錄,反之就是沒登錄3。
如果有多臺服務(wù)器,怎么判斷一個請求是否已經(jīng)登錄:這個問題涉及到分布式系統(tǒng)中的會話管理問題,一種常用的解決方案是使用單點登錄(Single Sign On, SSO),即用戶只需在一個系統(tǒng)中進(jìn)行登錄認(rèn)證,就可以訪問其他系統(tǒng)而無需再次登錄。SSO的實現(xiàn)方式有多種,例如使用OAuth協(xié)議、使用Redis存儲共享session、使用JWT(JSON Web Token)等12。
22.synchronize原理,自旋鎖等?系列鎖了解嗎
synchronize原理:synchronize是Java中的一個關(guān)鍵字,用于實現(xiàn)線程同步,保證多個線程對共享資源的操作是互斥的。synchronize的原理是基于對象內(nèi)部的一個監(jiān)視器鎖(monitor)來實現(xiàn)的,每個對象都有一個monitor,當(dāng)一個線程進(jìn)入synchronize修飾的方法或代碼塊時,就會獲取該對象的monitor,其他線程就無法進(jìn)入該方法或代碼塊,直到持有monitor的線程退出并釋放monitor。synchronize可以保證原子性、可見性和有序性123。
自旋鎖等一系列鎖了解嗎:自旋鎖是一種鎖的優(yōu)化策略,它的思想是當(dāng)一個線程嘗試獲取鎖時,如果鎖已經(jīng)被占用,就不會立即阻塞,而是在循環(huán)中不斷嘗試獲取鎖,直到成功或超時。自旋鎖適用于鎖占用時間較短、線程數(shù)不多的場景,可以避免線程切換帶來的開銷。但是如果鎖占用時間較長或者線程數(shù)較多,自旋鎖會消耗CPU資源,降低性能。自旋鎖有多種實現(xiàn)方式,例如CAS(Compare And Swap)操作、Ticket Lock、CLH Lock等??。除了自旋鎖之外,還有其他類型的鎖,例如悲觀鎖、樂觀鎖、公平鎖、非公平鎖、可重入鎖、獨占鎖、共享鎖,偏向鎖,輕量級鎖等2?。
偏向鎖是Java虛擬機對synchronized關(guān)鍵字的一種優(yōu)化策略,它利用對象頭中的標(biāo)志位來記錄第一個獲取該對象鎖的線程ID,如果該線程再次請求該對象鎖,就不需要進(jìn)行CAS操作來獲取鎖,而是直接進(jìn)入同步代碼塊。如果有其他線程嘗試獲取該對象鎖,就會觸發(fā)偏向鎖的撤銷,將偏向鎖升級為輕量級鎖或重量級鎖 。
- 輕量級鎖需要做CAS嗎:是的。輕量級鎖是一種利用CAS操作來實現(xiàn)的鎖,它的目的是在沒有多線程競爭的情況下,減少重量級鎖的開銷。輕量級鎖的加鎖過程是通過CAS操作將對象頭中的Mark Word替換為指向當(dāng)前線程棧中的鎖記錄(Lock Record)的指針,如果替換成功,則表示獲取鎖成功,否則表示有其他線程競爭鎖,需要升級為重量級鎖123。
- CAS操作是什么:CAS(Compare And Swap)操作是一種原子性的比較并替換操作,它需要三個參數(shù):內(nèi)存地址V、舊值A(chǔ)、新值B。CAS操作會比較內(nèi)存地址V中的值是否等于舊值A(chǔ),如果相等,則用新值B替換舊值A(chǔ),并返回true,否則不做任何操作,并返回false。CAS操作可以保證多線程對共享變量的更新不會發(fā)生沖突??。
23.java中 G1、CMS是什么
Java中G1、CMS是什么:G1和CMS都是Java虛擬機中的垃圾收集器,它們的目的是回收堆內(nèi)存中不再使用的對象,以釋放空間給新的對象。G1和CMS有一些共同點,比如都支持并發(fā)和分代收集,都關(guān)注降低垃圾回收的停頓時間,都適用于服務(wù)端應(yīng)用。但它們也有一些區(qū)別,比如:
- G1是基于Region(區(qū)域)的垃圾收集器,它將堆內(nèi)存劃分為多個大小相等的獨立區(qū)域,每個區(qū)域可以屬于新生代或老年代,這樣可以避免在整個堆范圍內(nèi)進(jìn)行回收。CMS是基于分代模型的垃圾收集器,它將堆內(nèi)存劃分為新生代和老年代,每個代可以由多個不連續(xù)的空間組成12。
- G1是基于標(biāo)記-整理算法的垃圾收集器,它在回收時不會產(chǎn)生內(nèi)存碎片,而且可以根據(jù)用戶指定的停頓時間來制定回收計劃,從而達(dá)到可預(yù)測的停頓時間。CMS是基于標(biāo)記-清除算法的垃圾收集器,它在回收時會產(chǎn)生內(nèi)存碎片,而且無法處理浮動垃圾(并發(fā)清理階段產(chǎn)生的垃圾),可能導(dǎo)致Concurrent Mode Failure(并發(fā)模式失敗)123。
- G1是Java 9之后的默認(rèn)垃圾收集器,它是CMS的長期替代方案,也是未來發(fā)展的方向。CMS已經(jīng)在Java 14中被廢棄? 。
- CMS是把堆空間劃分為新生代和老年代,每個代可以由多個不連續(xù)的空間組成,但是在回收時,CMS會對整個新生代或老年代進(jìn)行掃描,而不是只針對某個空間。G1則是把堆空間劃分為多個大小相等的獨立區(qū)域,每個區(qū)域可以屬于新生代或老年代,這樣在回收時,G1可以根據(jù)區(qū)域的回收價值和成本來選擇性地回收部分區(qū)域,而不是對整個堆進(jìn)行掃描。這樣可以減少垃圾回收的停頓時間和開銷。
24.rabbit內(nèi)部存儲結(jié)構(gòu)是什么
RabbitMQ存儲層包含兩個部分:隊列索引和消息存儲。
RabbitMQ消息有兩種類型:持久化消息和非持久化消息,這兩種消息都會被寫入磁盤。
持久化消息在到達(dá)隊列時寫入磁盤,同時會內(nèi)存中保存一份備份,當(dāng)內(nèi)存吃緊時,消息從內(nèi)存中清除。非持久化消息一般只存于內(nèi)存中,當(dāng)內(nèi)存吃緊時會被換入磁盤,以節(jié)省內(nèi)存空間。
RabbitMQ中的持久化消息和非持久化消息是由用戶聲明的12。持久化消息會被保存在磁盤中,不會因為RabbitMQ服務(wù)重啟或崩潰而丟失23。非持久化消息只會在內(nèi)存中存儲,當(dāng)內(nèi)存不足時可能會被寫入磁盤,但是重啟后就會消失2。持久化消息的性能要低于非持久化消息,因為需要寫入磁盤2。
持久化隊列和非持久化隊列的區(qū)別是,持久化隊列會被保存在磁盤中,固定并持久的存儲,當(dāng)Rabbit服務(wù)重啟后,該隊列會保持原來的狀態(tài)在RabbitMQ中被管理13?,而非持久化隊列不會被保存在磁盤中,Rabbit服務(wù)重啟后隊列就會消失13?。非持久化隊列的性能要高于持久化隊列,因為不需要寫入磁盤1。持久化隊列的優(yōu)點是會一直存在,不會隨服務(wù)的重啟或服務(wù)器的宕機而消失1。
持久化隊列和非持久化隊列中保存的消息可以是持久化消息也可以是非持久化消息1。持久化消息會同時寫入磁盤和內(nèi)存,加快讀取速度1。非持久化消息一般只保存在內(nèi)存中,在內(nèi)存吃緊的時候會被寫入到磁盤中,以節(jié)省內(nèi)存空間1。這兩種類型的消息的落盤處理都在RabbitMQ的"持久層"中完成1。
非持久化隊列中的消息,如果用戶指定了持久化消息,仍然要保存到磁盤中的。但是,如果RabbitMQ服務(wù)重啟或崩潰,非持久化隊列本身會消失,所以保存在磁盤中的消息也就無法被消費了。
隊列索引:rabbit_queue_index(下文簡稱index)
index維護(hù)隊列的落盤消息的信息,如存儲地點、是否已被交付給消費者、是否已被消費者ack等。每個隊列都有相對應(yīng)的index。
index使用順序的段文件來存儲,后綴為.idx,文件名從0開始累加,每個段文件中包含固定的segment_entry_count條記錄,默認(rèn)值是16384。每個index從磁盤中讀取消息的時候,至少要在內(nèi)存中維護(hù)一個段文件,所以設(shè)置queue_index_embed_msgs_below值得時候要格外謹(jǐn)慎,一點點增大也可能會引起內(nèi)存爆炸式增長。
消息存儲:rabbit_msg_store(下文簡稱store)
store以鍵值的形式存儲消息,所有隊列共享同一個store,每個節(jié)點有且只有一個。 從技術(shù)層面上說,store還可分為msg_store_persistent和msg_store_transient,前者負(fù)責(zé)持久化消息的持久化,重啟后消息不會丟失;后者負(fù)責(zé)非持久化消息的持久化,重啟后消息會丟失。通常情況下,兩者習(xí)慣性的被當(dāng)作一個整體。
store使用文件來存儲,后綴為.rdq,經(jīng)過store處理的所有消息都會以追加的方式寫入到該文件中,當(dāng)該文件的大小超過指定的限制(file_size_limit)后,將會關(guān)閉該文件并創(chuàng)建一個新的文件以供新的消息寫入。文件名從0開始進(jìn)行累加。在進(jìn)行消息的存儲時,RabbitMQ會在ETS(Erlang Term Storage)表中記錄消息在文件中的位置映射和文件的相關(guān)信息。
消息(包括消息頭、消息體、屬性)可以直接存儲在index中,也可以存儲在store中。最佳的方式是較小的消息存在index中,而較大的消息存在store中。這個消息大小的界定可以通過queue_index_embed_msgs_below來配置,默認(rèn)值為4096B。當(dāng)一個消息小于設(shè)定的大小閾值時,就可以存儲在index中,這樣性能上可以得到優(yōu)化(可理解為數(shù)據(jù)庫的覆蓋索引和回表)。 讀取消息時,先根據(jù)消息的ID(msg_id)找到對應(yīng)存儲的文件,如果文件存在并且未被鎖住,則直接打開文件,從指定位置讀取消息內(nèi)容。如果文件不存在或者被鎖住了,則發(fā)送請求由store進(jìn)行處理。
刪除消息時,只是從ETS表刪除指定消息的相關(guān)信息,同時更新消息對應(yīng)的存儲文件和相關(guān)信息。在執(zhí)行消息刪除操作時,并不立即對文件中的消息進(jìn)行刪除,也就是說消息依然在文件中,僅僅是標(biāo)記為垃圾數(shù)據(jù)而已。當(dāng)一個文件中都是垃圾數(shù)據(jù)時可以將這個文件刪除。當(dāng)檢測到前后兩個文件中的有效數(shù)據(jù)可以合并成一個文件,并且所有的垃圾數(shù)據(jù)的大小和所有文件(至少有3個文件存在的情況下)的數(shù)據(jù)大小的比值超過設(shè)置的閾值garbage_fraction(默認(rèn)值0.5)時,才會觸發(fā)垃圾回收,將這兩個文件合并,執(zhí)行合并的兩個文件一定是邏輯上相鄰的兩個文件。
25.zset是用什么實現(xiàn)的
有序集合(zset)同樣使用了兩種不同的存儲結(jié)構(gòu),分別是 zipList(壓縮列表)和 skipList(跳躍列表),當(dāng) zset 滿足以下條件時使用壓縮列表:
ziplist:滿足以下兩個條件的時候
- 元素數(shù)量少于128的時候
- 每個元素的長度小于64字節(jié)
skiplist:不滿足上述兩個條件就會使用跳表,具體來說是組合了map和skiplist
- map用來存儲member到score的映射,這樣就可以在O(1)時間內(nèi)找到member對應(yīng)的分?jǐn)?shù)
- skiplist按從小到大的順序存儲分?jǐn)?shù)
- skiplist每個元素的值都是[score,value]對
skiplist本質(zhì)上是并行的有序鏈表,但它克服了有序鏈表插入和查找性能不高的問題。它的插入和查詢的時間復(fù)雜度都是O(logN)
普通有序鏈表的插入需要一個一個向前查找是否可以插入,所以時間復(fù)雜度為O(N),比如下面這個鏈表插入23,就需要一直查找到22和26之間。
在上面這個結(jié)構(gòu)中,插入23的過程是
- 先使用第2層鏈接head->7->19->26,發(fā)現(xiàn)26比23大,就回到19
- 再用第1層連接19->22->26,發(fā)現(xiàn)比23大,那么就插入到26之前,22之后
上面這張圖就是跳表的初步原理,但一個元素插入鏈表后,應(yīng)該擁有幾層連接呢?跳表在這塊的實現(xiàn)方式是隨機的,也就是23這個元素插入后,隨機出一個數(shù),比如這個數(shù)是3,那么23就會有如下連接:
- 第3層head->23->end
- 第2層19->23->26
- 第1層22->23->26
下面這張圖展示了如何形成一個跳表
總結(jié)一下跳表原理:
每個跳表都必須設(shè)定一個最大的連接層數(shù)MaxLevel
第一層連接會連接到表中的每個元素
插入一個元素會隨機生成一個連接層數(shù)值[1, MaxLevel]之間,根據(jù)這個值跳表會給這元素建立連接
插入某個元素的時候先從最高層開始,當(dāng)跳到比目標(biāo)值大的元素后,回退到上一個元素,用該元素的下一層連接進(jìn)行遍歷,周而復(fù)始直到第一層連接,最終在第一層連接中找到合適的位置
redis中skiplist的MaxLevel設(shè)定為32層
skiplist原理中提到skiplist一個元素插入后,會隨機分配一個層數(shù),而redis的實現(xiàn),這個隨機的規(guī)則是:
一個元素?fù)碛械?層連接的概率為100%
一個元素?fù)碛械?層連接的概率為50%
一個元素?fù)碛械?層連接的概率為25%
以此類推...
為了提高搜索效率,redis會緩存MaxLevel的值,在每次插入/刪除節(jié)點后都會去更新這個值,這樣每次搜索的時候不需要從32層開始搜索,而是從MaxLevel指定的層數(shù)開始搜索
這句話是關(guān)于跳躍表的一種優(yōu)化方法,跳躍表是一種用來實現(xiàn)有序集合的數(shù)據(jù)結(jié)構(gòu),它由多層鏈表組成,每一層鏈表都是前一層鏈表的子集,每個結(jié)點都有一個指向下一層的指針。跳躍表的查詢效率很高,因為它可以從高層鏈表開始搜索,然后逐漸降低層級,直到找到目標(biāo)結(jié)點。但是如果每次搜索都從最高層開始,而最高層的結(jié)點數(shù)很少,那么就會浪費很多時間。所以為了提高搜索效率,redis會緩存MaxLevel的值,這個值表示當(dāng)前跳躍表的最高有效層級,也就是最高層中有兩個或以上的結(jié)點的層級。這樣每次搜索的時候就不需要從32層開始搜索,而是從MaxLevel指定的層數(shù)開始搜索23。這樣可以減少不必要的比較次數(shù),提高搜索效率。
26.CAS和版本號
樂觀鎖和悲觀鎖是兩種并發(fā)控制的策略,樂觀鎖認(rèn)為數(shù)據(jù)在一般情況下不會造成沖突,所以在訪問數(shù)據(jù)時不會加鎖,而是在更新數(shù)據(jù)時判斷是否有其他線程修改過數(shù)據(jù),如果有則重試或者放棄操作;悲觀鎖認(rèn)為數(shù)據(jù)在一般情況下會造成沖突,所以在訪問數(shù)據(jù)時會加鎖,防止其他線程修改數(shù)據(jù),直到操作完成后釋放鎖。樂觀鎖和悲觀鎖各有優(yōu)缺點,適用于不同的場景。樂觀鎖適合讀多寫少的場景,可以減少鎖的開銷,提高并發(fā)性能;悲觀鎖適合寫多讀少的場景,可以避免數(shù)據(jù)不一致的問題,保證數(shù)據(jù)的安全性。
樂觀鎖的實現(xiàn)方式有兩種:CAS(Compare And Swap)和版本號機制。CAS是一種原子操作,它需要三個參數(shù):內(nèi)存地址V,預(yù)期值A(chǔ)和新值B。CAS指令執(zhí)行時,比較內(nèi)存地址V的值是否等于預(yù)期值A(chǔ),如果相等則將新值B寫入V,否則不做任何操作。CAS可以保證共享變量的原子性,但是也有一些缺點,比如ABA問題(即一個值被修改了兩次又恢復(fù)原值),自旋開銷(即如果更新失敗則不斷重試),以及只能操作一個變量12。
版本號機制是另一種實現(xiàn)樂觀鎖的方式,它需要在數(shù)據(jù)表中增加一個版本號字段,每次更新數(shù)據(jù)時,將版本號加一,并且檢查更新前后的版本號是否一致。如果一致,則說明沒有其他線程修改過數(shù)據(jù),更新成功;如果不一致,則說明有其他線程修改過數(shù)據(jù),更新失敗3?。版本號機制可以避免ABA問題,也可以操作多個變量,但是也需要額外的空間來存儲版本號,并且可能存在并發(fā)沖突導(dǎo)致更新失敗的情況。
CAS只能保證單個變量的原子性,是因為它的底層實現(xiàn)是基于CPU的cmpxchg指令,這個指令只能對一個內(nèi)存地址進(jìn)行比較和交換操作2?。如果要對多個變量進(jìn)行原子操作,就需要使用鎖或者其他的方式13?。CAS只能操作一個變量,也是它的一個缺點之一,除此之外,CAS還可能存在ABA問題,自旋開銷和并發(fā)沖突等問題1?。
27.mysql怎么處理并發(fā)訪問呢
mysql處理并發(fā)訪問的主要方式是通過讀寫鎖和多版本并發(fā)控制 (MVCC)。1
讀寫鎖可以分為表鎖和行鎖,表鎖會鎖定整張表,行鎖會鎖定某一行數(shù)據(jù)。12
多版本并發(fā)控制 (MVCC) 是一種優(yōu)化并發(fā)讀寫的機制,它可以讓讀操作不用加鎖,而是根據(jù)每行記錄的版本號來判斷數(shù)據(jù)的可見性。1
mysql的并發(fā)控制也受到存儲引擎的影響,不同的存儲引擎有不同的實現(xiàn)方式。3
MVCC是一種不加鎖的讀取方式,它可以讓讀操作和寫操作并行進(jìn)行,提高數(shù)據(jù)庫的性能。12
但是MVCC并不是完全不需要加鎖,它只能避免讀寫沖突,也就是快照讀不加鎖,但是寫寫沖突還是需要加鎖的,也就是當(dāng)前讀需要加鎖。3?
當(dāng)前讀是指對數(shù)據(jù)進(jìn)行修改的操作,例如select ... for update, update, delete, insert等語句,這些語句都會對數(shù)據(jù)加排他鎖,防止其他事務(wù)修改同一條記錄。??
快照讀是指對數(shù)據(jù)進(jìn)行查詢的操作,例如select語句,這些語句不會對數(shù)據(jù)加鎖,而是根據(jù)記錄的版本號來判斷數(shù)據(jù)是否可見。??
MVCC只能工作在REPEATABLE READ (可重復(fù)讀)和READ COMMITED (提交讀)兩種事務(wù)隔離級別下。其他兩個隔離級別都與MVCC不兼容,因為READ UNCOMMITED (未提交讀)總是讀取最新的數(shù)據(jù)行,而不是符合當(dāng)前事務(wù)版本的數(shù)據(jù)行;而SERIALIZABLE則會對所有讀取的行都加鎖,也不符合MVCC的思想。
InnoDB通過為每一行記錄添加兩個額外的隱藏的值來實現(xiàn)MVCC,這兩個值一個記錄這行數(shù)據(jù)何時被創(chuàng)建,另外一個記錄這行數(shù)據(jù)何時過期(或者被刪除)。但是InnoDB并不存儲這些事件發(fā)生時的實際時間,相反它只存儲這些事件發(fā)生時的系統(tǒng)版本號(LSN)。這是一個隨著事務(wù)的創(chuàng)建而不斷增長的數(shù)字。每個事務(wù)在事務(wù)開始時會記錄它自己的系統(tǒng)版本號。每個查詢必須去檢查每行數(shù)據(jù)的版本號與事務(wù)的版本號是否相同。
28.線程通信與進(jìn)程通信
線程通信和進(jìn)程通信是兩種不同的并發(fā)編程的方式,它們的區(qū)別和聯(lián)系如下:
- 線程通信是指同一進(jìn)程內(nèi)的不同線程之間的信息交換,它們可以共享進(jìn)程的地址空間和資源,因此通信效率高,但需要注意同步和互斥的問題。12
- 進(jìn)程通信是指不同進(jìn)程之間的信息交換,它們擁有獨立的地址空間和資源,因此通信效率低,但安全性高,需要借助操作系統(tǒng)提供的通信機制,如管道、消息隊列、共享內(nèi)存、信號量、信號、套接字等。13
- 線程通信的目的主要是用于線程同步,保證數(shù)據(jù)的一致性和正確性,而進(jìn)程通信的目的主要是用于數(shù)據(jù)交換,實現(xiàn)不同進(jìn)程之間的協(xié)作和功能拓展。2?
- 線程通信和進(jìn)程通信都可以提高程序的并發(fā)度和性能,但也增加了編程的復(fù)雜度和開銷,需要根據(jù)具體的應(yīng)用場景和需求來選擇合適的方式。12
29.Java鎖的底層實現(xiàn)
Java鎖的底層實現(xiàn)主要依賴于AbstractQueuedSynchronizer(簡稱AQS)類,它是一個抽象類,提供了一套基于CAS和雙向鏈表的同步器框架。AQS中維護(hù)了一個狀態(tài)值和一個等待隊列,狀態(tài)值用于表示鎖的占用情況,等待隊列用于存儲等待獲取鎖的線程節(jié)點。AQS定義了一些模板方法,如acquire、release、tryAcquire、tryRelease等,具體的鎖實現(xiàn)類,如ReentrantLock、ReadWriteLock等,需要繼承AQS并實現(xiàn)其中的抽象方法。
當(dāng)一個線程調(diào)用lock方法時,它會首先嘗試通過CAS操作修改狀態(tài)值,如果成功,則表示獲取到了鎖;如果失敗,則表示鎖已經(jīng)被其他線程占用,此時它會將自己封裝成一個Node節(jié)點,通過自旋和CAS操作將自己加入到等待隊列的尾部。然后它會調(diào)用L
剩余60%內(nèi)容,訂閱專欄后可繼續(xù)查看/也可單篇購買
曾獲多國內(nèi)大廠的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。專注后端高頻面試與八股知識點,內(nèi)容系統(tǒng)詳實,覆蓋約 30 萬字面試真題解析、近 400 個熱點問題(包含大量場景題),60 萬字后端核心知識(含計網(wǎng)、操作系統(tǒng)、數(shù)據(jù)庫、性能調(diào)優(yōu)等)。同時提供簡歷優(yōu)化、HR 問題應(yīng)對、自我介紹等通用能力??紤]到歷史格式混亂、質(zhì)量較低、也在本地積累了大量資料,故準(zhǔn)備從頭重構(gòu)專欄全部內(nèi)容