去騰訊面試了,我自信滿滿!

ITPUB社群發表於2023-12-05


來源:小林coding


大家好,我是小林。

週六繼續捲起來,今天分享一位同學騰訊Java 後端的面經。

摘除了專案拷打的部分,把比較經典的問題給大家做了個總結,面試範圍主要是Java 基礎+Java 併發+JVM+MySQL+Redis+作業系統+演算法,整體上難度一般,可能是專案拷打的比較多,八股就隨便問問了。

Java

抽象類和普通類區別?

  • 例項化:普通類可以直接例項化物件,而抽象類不能被例項化,只能被繼承。
  • 方法實現:普通類中的方法可以有具體的實現,而抽象類中的方法可以有實現也可以沒有實現。
  • 繼承:一個類可以繼承一個普通類,而且可以繼承多個介面;而一個類只能繼承一個抽象類,但可以同時實現多個介面。
  • 實現限制:普通類可以被其他類繼承和使用,而抽象類一般用於作為基類,被其他類繼承和擴充套件使用。

抽象類和介面的區別?

相同點:

  • 都不能被例項化,介面的實現類或抽象類的子類都只有實現了介面或抽象類中的方法後才能例項化。

不同點:

  • 實現方式:實現介面的關鍵字為implements,繼承抽象類的關鍵字為extends。一個類可以實現多個介面,但一個類只能繼承一個抽象類。所以,使用介面可以間接地實現多重繼承。

  • 方法方式:介面只有定義,不能有方法的實現,java 1.8中可以定義default方法體,而抽象類可以有定義與實現,方法可在抽象類中實現。

  • 訪問修飾符:介面成員變數預設為public static final,必須賦初值,不能被修改;其所有的成員方法都是public、abstract的。抽象類中成員變數預設default,可在子類中被重新定義,也可被重新賦值;抽象方法被abstract修飾,不能被private、static、synchronized和native等修飾,必須以分號結尾,不帶花括號。

  • 變數:抽象類可以包含例項變數和靜態變數,而介面只能包含常量(即靜態常量)。

抽象類能加final修飾嗎?

不能,Java中的抽象類是用來被繼承的,而final修飾符用於禁止類被繼承或方法被重寫,因此,抽象類和final修飾符是互斥的,不能同時使用。

類載入過程是怎麼樣的?

我們編寫好的Java程式碼,經過編譯變成.class檔案,然後類載入器把.class位元組碼檔案載入到JVM中,接著執行我們的程式碼,最後將類解除安裝出JVM。

而從類載入到虛擬機器到解除安裝出虛擬機器的這一整個生命週期總共可以分為7個步驟,分別為載入、驗證、準備、解析、初始化、使用和解除安裝,其中驗證、準備和解析又稱為連線階段。

去騰訊面試了,我自信滿滿!
  • 載入階段:將需要用到的類對應的.class位元組碼檔案載入到虛擬機器記憶體,並在方法區中生成一個java.lang.Class物件,作為程式訪問這個類的各種資料的訪問入口。
  • 驗證階段:校驗載入進來的.class檔案中的內容是否符合規範,畢竟編譯成.class檔案後還是可以人為的對這個檔案進行修改,那如果改的亂七八糟,壓根不符合虛擬機器的規範,那虛擬機器就沒法執行了
  • 準備階段:準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配
  • 解析階段:在解析階段,將符號引用轉換為直接引用。符號引用指的是用符號表示的方法、欄位、類等,而直接引用是記憶體地址的指標。
  • 初始化階段:在初始化階段,執行類的初始化程式碼,包括靜態變數的賦值和靜態程式碼塊的執行。當類被首次主動使用時,即觸發初始化,而被動使用(如引用常量)不會觸發初始化。
  • 解除安裝階段:是類的生命週期中的最後一階段,即將方法區中無用的類回收

hashtable和hashmap區別是什麼?

  • hashmap不是執行緒安全的,HashMap 是 map 介面的實現類,是將鍵對映到值的物件,其中鍵和值都是物件,並且不能包含重複鍵,但可以包含重複值,HashMap 允許 null  key 和 null value

  • hashtable 是執行緒安全的,HashMap 是 HashTable 的輕量級實現,他們都完成了Map 介面,hashtable不允許 null  key 和 null value,由於非執行緒安全,效率上可能高於 Hashtable。

HashTable執行緒安全是怎麼實現的?

因為它的put,get做成了同步方法,保證了Hashtable的執行緒安全性,每個運算元據的方法都進行同步控制之後,由此帶來的問題任何一個時刻只能有一個執行緒可以操縱Hashtable,所以其效率比較低

Hashtable 的 put(K key, V value) 和 get(Object key) 方法的原始碼:

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
    throw new NullPointerException();
}
 // Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
    if ((entry.hash == hash) && entry.key.equals(key)) {
        V old = entry.value;
        entry.value = value;
        return old;
    }
}
 addEntry(hash, key, value, index);
return null;
}

public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {
        return (V)e.value;
    }
}
return null;
}

可以看到,Hashtable是透過使用了 synchronized 關鍵字來保證其執行緒安全

在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。

MySQL

innodb和myisam區別?

  • 事務:InnoDB 支援事務,MyISAM 不支援事務,這是 MySQL 將預設儲存引擎從 MyISAM 變成 InnoDB 的重要原因之一。
  • 索引結構:InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的檔案存放在主鍵索引的葉子節點上,因此 InnoDB 必須要有主鍵,透過主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然後再透過主鍵查詢到資料。因此,主鍵不應該過大,因為主鍵太大,其他索引也都會很大。而 MyISAM 是非聚集索引,資料檔案是分離的,索引儲存的是資料檔案的指標。主鍵索引和輔助索引是獨立的。
  • 鎖粒度:InnoDB 最小的鎖粒度是行鎖,MyISAM 最小的鎖粒度是表鎖。一個更新語句會鎖住整張表,導致其他查詢和更新都會被阻塞,因此併發訪問受限。
  • count 的效率:InnoDB 不儲存表的具體行數,執行 select count(*) from table 時需要全表掃描。而MyISAM 用一個變數儲存了整個表的行數,執行上述語句時只需要讀出該變數即可,速度很快。

B+樹原理以及和B樹的區別?

去騰訊面試了,我自信滿滿!

B 樹和 B+ 都是透過多叉樹的方式,會將樹的高度變矮,所以這兩個資料結構非常適合檢索存於磁碟中的資料。

但是 MySQL 預設的儲存引擎 InnoDB 採用的是 B+ 作為索引的資料結構,原因有:

  • B+ 樹的非葉子節點不存放實際的記錄資料,僅存放索引,因此資料量相同的情況下,相比儲存即存索引又存記錄的 B 樹,B+樹的非葉子節點可以存放更多的索引,因此 B+ 樹可以比 B 樹更「矮胖」,查詢底層節點的磁碟 I/O次數會更少。
  • B+ 樹有大量的冗餘節點(所有非葉子節點都是冗餘索引),這些冗餘索引讓 B+ 樹在插入、刪除的效率都更高,比如刪除根節點的時候,不會像 B 樹那樣會發生複雜的樹的變化;
  • B+ 樹葉子節點之間用連結串列連線了起來,有利於範圍查詢,而 B 樹要實現範圍查詢,因此只能透過樹的遍歷來完成範圍查詢,這會涉及多個節點的磁碟 I/O 操作,範圍查詢效率不如 B+ 樹。

mysql回表是什麼?

如果我用 product_no 二級索引查詢商品,如下查詢語句:

select * from product where product_no = '0002';

會先檢二級索引中的 B+Tree 的索引值(商品編碼,product_no),找到對應的葉子節點,然後獲取主鍵值,然後再透過主鍵索引中的 B+Tree 樹查詢到對應的葉子節點,然後獲取整行資料。這個過程叫「回表」,也就是說要查兩個 B+Tree 才能查到資料。如下圖:

去騰訊面試了,我自信滿滿!

不過,當查詢的資料是能在二級索引的 B+Tree 的葉子節點裡查詢到,這時就不用再查主鍵索引查,比如下面這條查詢語句:

select id from product where product_no = '0002';

這種在二級索引的 B+Tree 就能查詢到結果的過程就叫作「覆蓋索引」,也就是隻需要查一個 B+Tree 就能找到資料。

索引失效場景有哪些?

會發生索引失效的情況:

  • 當我們使用左或者左右模糊匹配的時候,也就是 like %xx 或者 like %xx%這兩種方式都會造成索引失效;
  • 當我們在查詢條件中對索引列使用函式,就會導致索引失效。
  • 當我們在查詢條件中對索引列進行表示式計算,也是無法走索引的。
  • MySQL 在遇到字串和數字比較的時候,會自動把字串轉為數字,然後再進行比較。如果字串是索引列,而條件語句中的輸入引數是數字的話,那麼索引列會發生隱式型別轉換,由於隱式型別轉換是透過 CAST 函式實現的,等同於對索引列使用了函式,所以就會導致索引失效。
  • 聯合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優先的方式進行索引的匹配,否則就會導致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 後的條件列不是索引列,那麼索引會失效。

資料庫鎖按資料操作的顆粒度的分為哪幾類?

  • 全域性鎖:透過flush tables with read lock 語句會將整個資料庫就處於只讀狀態了,這時其他執行緒執行以下操作,增刪改或者表結構修改都會阻塞。全域性鎖主要應用於做全庫邏輯備份,這樣在備份資料庫期間,不會因為資料或表結構的更新,而出現備份檔案的資料與預期的不一樣。
  • 表級鎖:MySQL 裡面表級別的鎖有這幾種:
    • 表鎖:透過lock tables 語句可以對錶加表鎖,表鎖除了會限制別的執行緒的讀寫外,也會限制本執行緒接下來的讀寫操作。
    • 後設資料鎖:當我們對資料庫表進行操作時,會自動給這個表加上 MDL,對一張表進行 CRUD 操作時,加的是 MDL 讀鎖;對一張表做結構變更操作的時候,加的是 MDL 寫鎖;MDL 是為了保證當使用者對錶執行 CRUD 操作時,防止其他執行緒對這個表結構做了變更。
    • 意向鎖:當執行插入、更新、刪除操作,需要先對錶加上「意向獨佔鎖」,然後對該記錄加獨佔鎖。意向鎖的目的是為了快速判斷表裡是否有記錄被加鎖
  • 行級鎖:InnoDB 引擎是支援行級鎖的,而 MyISAM 引擎並不支援行級鎖。
    • 記錄鎖,鎖住的是一條記錄。而且記錄鎖是有 S 鎖和 X 鎖之分的,滿足讀寫互斥,寫寫互斥
    • 間隙鎖,只存在於可重複讀隔離級別,目的是為了解決可重複讀隔離級別下幻讀的現象。
    • Next-Key Lock 稱為臨鍵鎖,是 Record Lock + Gap Lock 的組合,鎖定一個範圍,並且鎖定記錄本身。

Redis

redis資料型別有哪些?

去騰訊面試了,我自信滿滿!
  • String 型別的應用場景:快取物件、常規計數、分散式鎖、共享 session 資訊等。
  • List 型別的應用場景:訊息佇列(但是有兩個問題:1. 生產者需要自行實現全域性唯一 ID;2. 不能以消費組形式消費資料)等。
  • Hash 型別:快取物件、購物車等。
  • Set 型別:聚合計算(並集、交集、差集)場景,比如點贊、共同關注、抽獎活動等。
  • Zset 型別:排序場景,比如排行榜、電話和姓名排序等。

redis為什麼快?

官方使用基準測試的結果是,單執行緒的 Redis 吞吐量可以達到 10W/每秒,如下圖所示:

去騰訊面試了,我自信滿滿!

之所以 Redis 採用單執行緒(網路 I/O 和執行命令)那麼快,有如下幾個原因:

  • Redis 的大部分操作都在記憶體中完成,並且採用了高效的資料結構,因此 Redis 瓶頸可能是機器的記憶體或者網路頻寬,而並非 CPU,既然 CPU 不是瓶頸,那麼自然就採用單執行緒的解決方案了;
  • Redis 採用單執行緒模型可以避免了多執行緒之間的競爭,省去了多執行緒切換帶來的時間和效能上的開銷,而且也不會導致死鎖問題。
  • Redis 採用了 I/O 多路複用機制處理大量的客戶端 Socket 請求,IO 多路複用機制是指一個執行緒處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 只執行單執行緒的情況下,該機制允許核心中,同時存在多個監聽 Socket 和已連線 Socket。核心會一直監聽這些 Socket 上的連線請求或資料請求。一旦有請求到達,就會交給 Redis 執行緒處理,這就實現了一個 Redis 執行緒處理多個 IO 流的效果。

AOF與RDB持久化方式的區別?

  • 內容格式:AOF 以日誌追加的方式記錄所有寫操作,將命令以文字形式追加到檔案末尾;而 RDB 則是將 Redis 資料庫在某個時間點的快照以二進位制形式儲存到磁碟上。
  • 資料恢復速度:由於 AOF 記錄了所有的寫操作,資料恢復速度相對較慢,需要重新執行所有寫操作;而 RDB 是透過載入快照檔案來恢復資料,速度通常比 AOF 快。
  • 檔案大小:AOF 檔案通常會比 RDB 檔案大,因為它記錄了所有寫操作的文字形式,而 RDB 檔案只是儲存了資料庫快照的二進位制資料。
  • 容災能力:由於 AOF 記錄了所有寫操作,當 Redis 重啟時,可以透過重新執行 AOF 檔案中的命令來恢復資料,因此在發生故障時,資料丟失的可能性較小。而 RDB 是透過載入快照檔案恢復資料,如果最後一次儲存快照的時間點之後發生了故障,可能會導致資料丟失。

redis當機怎麼辦?

可以考慮使用 Redis 的高可用架構,如主從複製、哨兵模式或 Redis 叢集,以保證服務的持續可用性。

主從複製

主從複製是 Redis 高可用服務的最基礎的保證,實現方案就是將從前的一臺 Redis 伺服器,同步資料到多臺從 Redis 伺服器上,即一主多從的模式,且主從伺服器之間採用的是「讀寫分離」的方式。

主伺服器可以進行讀寫操作,當發生寫操作時自動將寫操作同步給從伺服器,而從伺服器一般是隻讀,並接受主伺服器同步過來寫操作命令,然後執行這條命令。

去騰訊面試了,我自信滿滿!

也就是說,所有的資料修改只在主伺服器上進行,然後將最新的資料同步給從伺服器,這樣就使得主從伺服器的資料是一致的。

注意,主從伺服器之間的命令複製是非同步進行的。

具體來說,在主從伺服器命令傳播階段,主伺服器收到新的寫命令後,會傳送給從伺服器。但是,主伺服器並不會等到從伺服器實際執行完命令後,再把結果返回給客戶端,而是主伺服器自己在本地執行完命令後,就會向客戶端返回結果了。如果從伺服器還沒有執行主伺服器同步過來的命令,主從伺服器間的資料就不一致了。

所以,無法實現強一致性保證(主從資料時時刻刻保持一致),資料不一致是難以避免的。

哨兵模式

在使用 Redis 主從服務的時候,會有一個問題,就是當 Redis 的主從伺服器出現故障當機時,需要手動進行恢復。

為了解決這個問題,Redis 增加了哨兵模式(Redis Sentinel),因為哨兵模式做到了可以監控主從伺服器,並且提供主從節點故障轉移的功能。

去騰訊面試了,我自信滿滿!

切片叢集模式

當 Redis 快取資料量大到一臺伺服器無法快取時,就需要使用 Redis 切片叢集(Redis Cluster )方案,它將資料分佈在不同的伺服器上,以此來降低系統對單主節點的依賴,從而提高 Redis 服務的讀寫效能。

Redis Cluster 方案採用雜湊槽(Hash Slot),來處理資料和節點之間的對映關係。在 Redis Cluster 方案中,一個切片叢集共有 16384 個雜湊槽,這些雜湊槽類似於資料分割槽,每個鍵值對都會根據它的 key,被對映到一個雜湊槽中,具體執行過程分為兩大步:

  • 根據鍵值對的 key,按照 CRC16 演算法計算一個 16 bit 的值。
  • 再用 16bit 值對 16384 取模,得到 0~16383 範圍內的模數,每個模數代表一個相應編號的雜湊槽。

接下來的問題就是,這些雜湊槽怎麼被對映到具體的 Redis 節點上的呢?有兩種方案:

  • 平均分配: 在使用 cluster create 命令建立 Redis 叢集時,Redis 會自動把所有雜湊槽平均分佈到叢集節點上。比如叢集中有 9 個節點,則每個節點上槽的個數為 16384/9 個。
  • 手動分配: 可以使用 cluster meet 命令手動建立節點間的連線,組成叢集,再使用 cluster addslots 命令,指定每個節點上的雜湊槽個數。

為了方便你的理解,我透過一張圖來解釋資料、雜湊槽,以及節點三者的對映分佈關係。

去騰訊面試了,我自信滿滿!

上圖中的切片叢集一共有 2 個節點,假設有 4 個雜湊槽(Slot 0~Slot 3)時,我們就可以透過命令手動分配雜湊槽,比如節點 1 儲存雜湊槽 0 和 1,節點 2 儲存雜湊槽 2 和 3。

redis-cli -h 192.168.1.10 –p 6379 cluster addslots 0,1
redis-cli -h 192.168.1.11 –p 6379 cluster addslots 2,3

然後在叢集執行的過程中,key1 和 key2 計算完 CRC16 值後,對雜湊槽總個數 4 進行取模,再根據各自的模數結果,就可以被對映到雜湊槽 1(對應節點1) 和 雜湊槽 2(對應節點2)。

需要注意的是,在手動分配雜湊槽時,需要把 16384 個槽都分配完,否則 Redis 叢集無法正常工作。

作業系統

自旋鎖是什麼?應用在哪些場景?

自旋鎖加鎖失敗後,執行緒會忙等待,直到它拿到鎖。

自旋鎖是透過 CPU 提供的 CAS 函式(Compare And Swap),在「使用者態」完成加鎖和解鎖操作,不會主動產生執行緒上下文切換,所以相比互斥鎖來說,會快一些,開銷也小一些。

一般加鎖的過程,包含兩個步驟:

  • 第一步,檢視鎖的狀態,如果鎖是空閒的,則執行第二步;
  • 第二步,將鎖設定為當前執行緒持有;

CAS 函式就把這兩個步驟合併成一條硬體級指令,形成原子指令,這樣就保證了這兩個步驟是不可分割的,要麼一次性執行完兩個步驟,要麼兩個步驟都不執行。

比如,設鎖為變數 lock,整數 0 表示鎖是空閒狀態,整數 pid 表示執行緒 ID,那麼 CAS(lock, 0, pid) 就表示自旋鎖的加鎖操作,CAS(lock, pid, 0) 則表示解鎖操作。

使用自旋鎖的時候,當發生多執行緒競爭鎖的情況,加鎖失敗的執行緒會「忙等待」,直到它拿到鎖。這裡的「忙等待」可以用 while 迴圈等待實現,不過最好是使用 CPU 提供的 PAUSE 指令來實現「忙等待」,因為可以減少迴圈等待時的耗電量。

自旋鎖是最比較簡單的一種鎖,一直自旋,利用 CPU 週期,直到鎖可用。需要注意,在單核 CPU 上,需要搶佔式的排程器(即不斷透過時鐘中斷一個執行緒,執行其他執行緒)。否則,自旋鎖在單 CPU 上無法使用,因為一個自旋的執行緒永遠不會放棄 CPU。

自旋鎖開銷少,在多核系統下一般不會主動產生執行緒切換,適合非同步、協程等在使用者態切換請求的程式設計方式,但如果被鎖住的程式碼執行時間過長,自旋的執行緒會長時間佔用 CPU 資源,所以自旋的時間和被鎖住的程式碼執行的時間是成「正比」的關係,我們需要清楚的知道這一點。

自旋鎖與互斥鎖使用層面比較相似,但實現層面上完全不同:當加鎖失敗時,互斥鎖用「執行緒切換」來應對,自旋鎖則用「忙等待」來應對

如果你能確定被鎖住的程式碼執行時間很短,就不應該用互斥鎖,而應該選用自旋鎖,否則使用互斥鎖。

演算法

  • 合併區間



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2998729/,如需轉載,請註明出處,否則將追究法律責任。

相關文章