查缺補漏2

這些問題是 2018 年去美團面試的同學被問到的一些常見的問題,希望對你有幫助!

一 基礎篇

1. System.out.println(3|9)輸出什麼?

正確答案:11。

考察知識點:&和&&;|和||

&和&&:

共同點:兩者都可做邏輯運算符。它們都表示運算符的兩邊都是 true 時,結果爲 true;

不同點: &也是位運算符。& 表示在運算時兩邊都會計算,然後再判斷;&&表示先運算符號左邊的東西,然後判斷是否爲 true,是 true 就繼續運算右邊的然後判斷並輸出,是 false 就停下來直接輸出不會再運行後面的東西。

|和||:

共同點:兩者都可做邏輯運算符。它們都表示運算符的兩邊任意一邊爲 true,結果爲 true,兩邊都不是 true,結果就爲 false;

不同點:|也是位運算符。| 表示兩邊都會運算,然後再判斷結果;|| 表示先運算符號左邊的東西,然後判斷是否爲 true,是 true 就停下來直接輸出不會再運行後面的東西,是 false 就繼續運算右邊的然後判斷並輸出。

回到本題:

3 | 9=0011(二進制) | 1001(二進制)=1011(二進制)=11(十進制)

2. 說一下轉發(Forward)和重定向(Redirect)的區別

轉發是服務器行爲,重定向是客戶端行爲。

轉發(Forword) 通過 RequestDispatcher 對象的forward(HttpServletRequest request,HttpServletResponse response)方法實現的。RequestDispatcher 可以通過HttpServletRequestgetRequestDispatcher()方法獲得。例如下面的代碼就是跳轉到 login_success.jsp 頁面。

request.getRequestDispatcher("login_success.jsp").forward(request, response);

重定向(Redirect) 是利用服務器返回的狀態碼來實現的。客戶端瀏覽器請求服務器的時候,服務器會返回一個狀態碼。服務器通過 HttpServletRequestResponse 的 setStatus(int status)方法設置狀態碼。如果服務器返回 301 或者 302,則瀏覽器會到新的網址重新請求該資源。

  1. 從地址欄顯示來說:forward 是服務器請求資源,服務器直接訪問目標地址的 URL,把那個 URL 的響應內容讀取過來,然後把這些內容再發給瀏覽器。瀏覽器根本不知道服務器發送的內容從哪裏來的,所以它的地址欄還是原來的地址。redirect 是服務端根據邏輯,發送一個狀態碼,告訴瀏覽器重新去請求那個地址。所以地址欄顯示的是新的 URL。
  2. 從數據共享來說:forward:轉發頁面和轉發到的頁面可以共享 request 裏面的數據。redirect:不能共享數據。
  3. 從運用地方來說:forward:一般用於用戶登陸的時候,根據角色轉發到相應的模塊。redirect:一般用於用戶註銷登陸時返回主頁面和跳轉到其它的網站等。
  4. 從效率來說:forward:高。redirect:低。

3. 在瀏覽器中輸入 url 地址到顯示主頁的過程,整個過程會使用哪些協議

圖片來源:《圖解 HTTP》:

各種網絡請求用到的協議

總體來說分爲以下幾個過程:

  1. DNS 解析
  2. TCP 連接
  3. 發送 HTTP 請求
  4. 服務器處理請求並返回 HTTP 報文
  5. 瀏覽器解析渲染頁面
  6. 連接結束

具體可以參考下面這篇文章:

修正 issue-568:上圖中 IP 數據包在路由器之間使用的協議爲 OPSF 協議錯誤,應該爲 OSPF 協議 。

IP 數據包在路由器之間傳播大致分爲 IGP 和 BGP 協議,而 IGP 目前主流爲 OSPF 協議,思科,華爲和 H3C 等主流廠商都有各自實現並使用;BGP 協議爲不同 AS(自治系統號)間路由傳輸,也分爲 I-BGP 和 E-BGP,詳細資料請查看《TCP/IP 卷一》

4. TCP 三次握手和四次揮手

爲了準確無誤地把數據送達目標處,TCP 協議採用了三次握手策略。

漫畫圖解:

圖片來源:《圖解 HTTP》

簡單示意圖:

  • 客戶端–發送帶有 SYN 標誌的數據包–一次握手–服務端
  • 服務端–發送帶有 SYN/ACK 標誌的數據包–二次握手–客戶端
  • 客戶端–發送帶有帶有 ACK 標誌的數據包–三次握手–服務端

爲什麼要三次握手

三次握手的目的是建立可靠的通信信道,說到通訊,簡單來說就是數據的發送與接收,而三次握手最主要的目的就是雙方確認自己與對方的發送與接收是正常的。

第一次握手:Client 什麼都不能確認;Server 確認了對方發送正常,自己接收正常。

第二次握手:Client 確認了:自己發送、接收正常,對方發送、接收正常;Server 確認了:自己接收正常,對方發送正常

第三次握手:Client 確認了:自己發送、接收正常,對方發送、接收正常;Server 確認了:自己發送、接收正常,對方發送、接收正常

所以三次握手就能確認雙發收發功能都正常,缺一不可。

爲什麼要傳回 SYN

接收端傳回發送端所發送的 SYN 是爲了告訴發送端,我接收到的信息確實就是你所發送的信號了。

SYN 是 TCP/IP 建立連接時使用的握手信號。在客戶機和服務器之間建立正常的 TCP 網絡連接時,客戶機首先發出一個 SYN 消息,服務器使用 SYN-ACK 應答表示接收到了這個消息,最後客戶機再以 ACK(Acknowledgement[漢譯:確認字符 ,在數據通信傳輸中,接收站發給發送站的一種傳輸控制字符。它表示確認發來的數據已經接受無誤。 ])消息響應。這樣在客戶機和服務器之間才能建立起可靠的 TCP 連接,數據纔可以在客戶機和服務器之間傳遞。

傳了 SYN,爲啥還要傳 ACK

雙方通信無誤必須是兩者互相發送信息都無誤。傳了 SYN,證明發送方(主動關閉方)到接收方(被動關閉方)的通道沒有問題,但是接收方到發送方的通道還需要 ACK 信號來進行驗證。

斷開一個 TCP 連接則需要“四次揮手”:

  • 客戶端-發送一個 FIN,用來關閉客戶端到服務器的數據傳送
  • 服務器-收到這個 FIN,它發回一 個 ACK,確認序號爲收到的序號加 1 。和 SYN 一樣,一個 FIN 將佔用一個序號
  • 服務器-關閉與客戶端的連接,發送一個 FIN 給客戶端
  • 客戶端-發回 ACK 報文確認,並將確認序號設置爲收到序號加 1

爲什麼要四次揮手

任何一方都可以在數據傳送結束後發出連接釋放的通知,待對方確認後進入半關閉狀態。當另一方也沒有數據再發送的時候,則發出連接釋放通知,對方確認後就完全關閉了 TCP 連接。

舉個例子:A 和 B 打電話,通話即將結束後,A 說“我沒啥要說的了”,B 回答“我知道了”,但是 B 可能還會有要說的話,A 不能要求 B 跟着自己的節奏結束通話,於是 B 可能又巴拉巴拉說了一通,最後 B 說“我說完了”,A 回答“知道了”,這樣通話纔算結束。

上面講的比較概括,推薦一篇講的比較細緻的文章:https://blog.csdn.net/qzcsu/article/details/72861891

5. IP 地址與 MAC 地址的區別

參考:https://blog.csdn.net/guoweimelon/article/details/50858597

IP 地址是指互聯網協議地址(Internet Protocol Address)IP Address 的縮寫。IP 地址是 IP 協議提供的一種統一的地址格式,它爲互聯網上的每一個網絡和每一臺主機分配一個邏輯地址,以此來屏蔽物理地址的差異。

MAC 地址又稱爲物理地址、硬件地址,用來定義網絡設備的位置。網卡的物理地址通常是由網卡生產廠家寫入網卡的,具有全球唯一性。MAC 地址用於在網絡中唯一標示一個網卡,一臺電腦會有一或多個網卡,每個網卡都需要有一個唯一的 MAC 地址。

6. HTTP 請求,響應報文格式

HTTP 請求報文主要由請求行、請求頭部、請求正文 3 部分組成

HTTP 響應報文主要由狀態行、響應頭部、響應正文 3 部分組成

詳細內容可以參考:https://blog.csdn.net/a19881029/article/details/14002273

7. 爲什麼要使用索引?索引這麼多優點,爲什麼不對錶中的每一個列創建一個索引呢?索引是如何提高查詢速度的?說一下使用索引的注意事項?Mysql 索引主要使用的兩種數據結構?什麼是覆蓋索引?

爲什麼要使用索引?

  1. 通過創建唯一性索引,可以保證數據庫表中每一行數據的唯一性。
  2. 可以大大加快 數據的檢索速度(大大減少的檢索的數據量), 這也是創建索引的最主要的原因。
  3. 幫助服務器避免排序和臨時表
  4. 將隨機 IO 變爲順序 IO
  5. 可以加速表和表之間的連接,特別是在實現數據的參考完整性方面特別有意義。

索引這麼多優點,爲什麼不對錶中的每一個列創建一個索引呢?

  1. 當對錶中的數據進行增加、刪除和修改的時候,索引也要動態的維護,這樣就降低了數據的維護速度。
  2. 索引需要佔物理空間,除了數據表佔數據空間之外,每一個索引還要佔一定的物理空間,如果要建立聚簇索引,那麼需要的空間就會更大。
  3. 創建索引和維護索引要耗費時間,這種時間隨着數據量的增加而增加。

索引是如何提高查詢速度的?

將無序的數據變成相對有序的數據(就像查目錄一樣)

說一下使用索引的注意事項

  1. 避免 where 子句中對字段施加函數,這會造成無法命中索引。
  2. 在使用 InnoDB 時使用與業務無關的自增主鍵作爲主鍵,即使用邏輯主鍵,而不要使用業務主鍵。
  3. 將打算加索引的列設置爲 NOT NULL ,否則將導致引擎放棄使用索引而進行全表掃描
  4. 刪除長期未使用的索引,不用的索引的存在會造成不必要的性能損耗 MySQL 5.7 可以通過查詢 sys 庫的 schema_unused_indexes 視圖來查詢哪些索引從未被使用
  5. 在使用 limit offset 查詢緩慢時,可以藉助索引來提高性能

Mysql 索引主要使用的哪兩種數據結構?

  • 哈希索引:對於哈希索引來說,底層的數據結構就是哈希表,因此在絕大多數需求爲單條記錄查詢的時候,可以選擇哈希索引,查詢性能最快;其餘大部分場景,建議選擇 BTree 索引。
  • BTree 索引:Mysql 的 BTree 索引使用的是 B 樹中的 B+Tree。但對於主要的兩種存儲引擎(MyISAM 和 InnoDB)的實現方式是不同的。

更多關於索引的內容可以查看我的這篇文章:【思維導圖-索引篇】搞定數據庫索引就是這麼簡單

什麼是覆蓋索引?

如果一個索引包含(或者說覆蓋)所有需要查詢的字段的值,我們就稱
之爲“覆蓋索引”。我們知道在 InnoDB 存儲引擎中,如果不是主鍵索引,葉子節點存儲的是主鍵+列值。最終還是要“回表”,也就是要通過主鍵再查找一次,這樣就會比較慢。覆蓋索引就是把要查詢出的列和索引是對應的,不做回表操作!

8. 進程與線程的區別是什麼?進程間的幾種通信方式說一下?線程間的幾種通信方式知道不?

進程與線程的區別是什麼?

線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享同一塊內存空間和一組系統資源,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因爲如此,線程也被稱爲輕量級進程。另外,也正是因爲共享資源,所以線程中執行時一般都要進行同步和互斥。總的來說,進程和線程的主要差別在於它們是不同的操作系統資源管理方式。

進程間的幾種通信方式說一下?

  1. 管道(pipe):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有血緣關係的進程間使用。進程的血緣關係通常指父子進程關係。管道分爲 pipe(無名管道)和 fifo(命名管道)兩種,有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間通信。
  2. 信號量(semophore):信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它通常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
  3. 消息隊列(message queue):消息隊列是由消息組成的鏈表,存放在內核中 並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。消息隊列與管道通信相比,其優勢是對每個消息指定特定的消息類型,接收的時候不需要按照隊列次序,而是可以根據自定義條件接收特定類型的消息。
  4. 信號(signal):信號是一種比較複雜的通信方式,用於通知接收進程某一事件已經發生。
  5. 共享內存(shared memory):共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問,共享內存是最快的 IPC 方式,它是針對其他進程間的通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量配合使用,來實現進程間的同步和通信。
  6. 套接字(socket):socket,即套接字是一種通信機制,憑藉這種機制,客戶/服務器(即要進行通信的進程)系統的開發工作既可以在本地單機上進行,也可以跨網絡進行。也就是說它可以讓不在同一臺計算機但通過網絡連接計算機上的進程進行通信。也因爲這樣,套接字明確地將客戶端和服務器區分開來。

線程間的幾種通信方式知道不?

1、鎖機制

  • 互斥鎖:提供了以排它方式阻止數據結構被併發修改的方法。
  • 讀寫鎖:允許多個線程同時讀共享數據,而對寫操作互斥。
  • 條件變量:可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。

2、信號量機制:包括無名線程信號量與有名線程信號量

3、信號機制:類似於進程間的信號處理。

線程間通信的主要目的是用於線程同步,所以線程沒有象進程通信中用於數據交換的通信機制。

9. 爲什麼要用單例模式?手寫幾種線程安全的單例模式?

簡單來說使用單例模式可以帶來下面幾個好處:

  • 對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷;
  • 由於 new 操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間。

懶漢式(雙重檢查加鎖版本)

public class Singleton {

    //volatile保證,當uniqueInstance變量被初始化成Singleton實例時,多個線程可以正確處理uniqueInstance變量
    private volatile static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
       //檢查實例,如果不存在,就進入同步代碼塊
        if (uniqueInstance == null) {
            //只有第一次才徹底執行這裏的代碼
            synchronized(Singleton.class) {
               //進入同步代碼塊後,再檢查一次,如果仍是null,才創建實例
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

靜態內部類方式

靜態內部實現的單例是懶加載的且線程安全。

只有通過顯式調用 getInstance 方法時,纔會顯式裝載 SingletonHolder 類,從而實例化 instance(只有第一次使用這個單例的實例的時候才加載,同時不會有線程安全問題)。

public class Singleton {
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
}

10. 簡單介紹一下 bean;知道 Spring 的 bean 的作用域與生命週期嗎?

在 Spring 中,那些組成應用程序的主體及由 Spring IOC 容器所管理的對象,被稱之爲 bean。簡單地講,bean 就是由 IOC 容器初始化、裝配及管理的對象,除此之外,bean 就與應用程序中的其他對象沒有什麼區別了。而 bean 的定義以及 bean 相互間的依賴關係將通過配置元數據來描述。

Spring 中的 bean 默認都是單例的,這些單例 Bean 在多線程程序下如何保證線程安全呢? 例如對於 Web 應用來說,Web 容器對於每個用戶請求都創建一個單獨的 Sevlet 線程來處理請求,引入 Spring 框架之後,每個 Action 都是單例的,那麼對於 Spring 託管的單例 Service Bean,如何保證其安全呢? Spring 的單例是基於 BeanFactory 也就是 Spring 容器的,單例 Bean 在此容器內只有一個,Java 的單例是基於 JVM,每個 JVM 內只有一個實例。

pring的bean的作用域

Spring 的 bean 的生命週期以及更多內容可以查看:一文輕鬆搞懂 Spring 中 bean 的作用域與生命週期

11. Spring 中的事務傳播行爲了解嗎?TransactionDefinition 接口中哪五個表示隔離級別的常量?

事務傳播行爲

事務傳播行爲(爲了解決業務層方法之間互相調用的事務問題):
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啓一個新事務,並在自己的事務中運行。在 TransactionDefinition 定義中包括瞭如下幾個表示傳播行爲的常量:

支持當前事務的情況:

  • TransactionDefinition.PROPAGATION_REQUIRED: 如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
  • TransactionDefinition.PROPAGATION_MANDATORY: 如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。(mandatory:強制性)

不支持當前事務的情況:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: 創建一個新的事務,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事務方式運行,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事務方式運行,如果當前存在事務,則拋出異常。

其他情況:

  • TransactionDefinition.PROPAGATION_NESTED: 如果當前存在事務,則創建一個事務作爲當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於 TransactionDefinition.PROPAGATION_REQUIRED。

隔離級別

TransactionDefinition 接口中定義了五個表示隔離級別的常量:

  • TransactionDefinition.ISOLATION_DEFAULT: 使用後端數據庫默認的隔離級別,Mysql 默認採用的 REPEATABLE_READ 隔離級別 Oracle 默認採用的 READ_COMMITTED 隔離級別.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致髒讀、幻讀或不可重複讀
  • TransactionDefinition.ISOLATION_READ_COMMITTED: 允許讀取併發事務已經提交的數據,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔離級別,完全服從 ACID 的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。

12. SpringMVC 原理了解嗎?

SpringMVC 原理

客戶端發送請求-> 前端控制器 DispatcherServlet 接受客戶端請求 -> 找到處理器映射 HandlerMapping 解析請求對應的 Handler-> HandlerAdapter 會根據 Handler 來調用真正的處理器處理請求,並處理相應的業務邏輯 -> 處理器返回一個模型視圖 ModelAndView -> 視圖解析器進行解析 -> 返回一個視圖對象->前端控制器 DispatcherServlet 渲染數據(Model)->將得到視圖對象返回給用戶

關於 SpringMVC 原理更多內容可以查看我的這篇文章:SpringMVC 工作原理詳解

13. Spring AOP IOC 實現原理

過了秋招挺長一段時間了,說實話我自己也忘了如何簡要概括 Spring AOP IOC 實現原理,就在網上找了一個較爲簡潔的答案,下面分享給各位。

IOC: 控制反轉也叫依賴注入。IOC 利用 java 反射機制,AOP 利用代理模式。IOC 概念看似很抽象,但是很容易理解。說簡單點就是將對象交給容器管理,你只需要在 spring 配置文件中配置對應的 bean 以及設置相關的屬性,讓 spring 容器來生成類的實例對象以及管理對象。在 spring 容器啓動的時候,spring 會把你在配置文件中配置的 bean 都初始化好,然後在你需要調用的時候,就把它已經初始化好的那些 bean 分配給你需要調用這些 bean 的類。

AOP: 面向切面編程。(Aspect-Oriented Programming) 。AOP 可以說是對 OOP 的補充和完善。OOP 引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行爲的一個集合。實現 AOP 的技術,主要分爲兩大類:一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼,屬於靜態代理。

二 進階篇

1 消息隊列 MQ 的套路

消息隊列/消息中間件應該是 Java 程序員必備的一個技能了,如果你之前沒接觸過消息隊列的話,建議先去百度一下某某消息隊列入門,然後花 2 個小時就差不多可以學會任何一種消息隊列的使用了。如果說僅僅學會使用是萬萬不夠的,在實際生產環境還要考慮消息丟失等等情況。關於消息隊列面試相關的問題,推薦大家也可以看一下視頻《Java 工程師面試突擊第 1 季-中華石杉老師》,如果大家沒有資源的話,可以在我的公衆號“Java 面試通關手冊”後臺回覆關鍵字“1”即可!

1.1 介紹一下消息隊列 MQ 的應用場景/使用消息隊列的好處

面試官一般會先問你這個問題,預熱一下,看你知道消息隊列不,一般在第一面的時候面試官可能只會問消息隊列 MQ 的應用場景/使用消息隊列的好處、使用消息隊列會帶來什麼問題、消息隊列的技術選型這幾個問題,不會太深究下去,在後面的第二輪/第三輪技術面試中可能會深入問一下。

《大型網站技術架構》第四章和第七章均有提到消息隊列對應用性能及擴展性的提升。

1)通過異步處理提高系統性能

通過異步處理提高系統性能
如上圖,在不使用消息隊列服務器的時候,用戶的請求數據直接寫入數據庫,在高併發的情況下數據庫壓力劇增,使得響應速度變慢。但是在使用消息隊列之後,用戶的請求數據發送給消息隊列之後立即 返回,再由消息隊列的消費者進程從消息隊列中獲取數據,異步寫入數據庫。由於消息隊列服務器處理速度快於數據庫(消息隊列也比數據庫有更好的伸縮性),因此響應速度得到大幅改善。

通過以上分析我們可以得出消息隊列具有很好的削峯作用的功能——即通過異步處理,將短時間高併發產生的事務消息存儲在消息隊列中,從而削平高峯期的併發事務。 舉例:在電子商務一些秒殺、促銷活動中,合理使用消息隊列可以有效抵禦促銷活動剛開始大量訂單涌入對系統的衝擊。如下圖所示:
合理使用消息隊列可以有效抵禦促銷活動剛開始大量訂單涌入對系統的衝擊
因爲用戶請求數據寫入消息隊列之後就立即返回給用戶了,但是請求數據在後續的業務校驗、寫數據庫等操作中可能失敗。因此使用消息隊列進行異步處理之後,需要適當修改業務流程進行配合,比如用戶在提交訂單之後,訂單數據寫入消息隊列,不能立即返回用戶訂單提交成功,需要在消息隊列的訂單消費者進程真正處理完該訂單之後,甚至出庫後,再通過電子郵件或短信通知用戶訂單成功,以免交易糾紛。這就類似我們平時手機訂火車票和電影票。

2)降低系統耦合性

我們知道模塊分佈式部署以後聚合方式通常有兩種:1.分佈式消息隊列和 2.分佈式服務

先來簡單說一下分佈式服務:

目前使用比較多的用來構建SOA(Service Oriented Architecture 面向服務體系結構)分佈式服務框架是阿里巴巴開源的Dubbo。如果想深入瞭解 Dubbo 的可以看我寫的關於 Dubbo 的這一篇文章:《高性能優秀的服務框架-dubbo 介紹》https://juejin.im/post/5acadeb1f265da2375072f9c

再來談我們的分佈式消息隊列:

我們知道如果模塊之間不存在直接調用,那麼新增模塊或者修改模塊就對其他模塊影響較小,這樣系統的可擴展性無疑更好一些。

我們最常見的事件驅動架構類似生產者消費者模式,在大型網站中通常用利用消息隊列實現事件驅動結構。如下圖所示:

利用消息隊列實現事件驅動結構
消息隊列使利用發佈-訂閱模式工作,消息發送者(生產者)發佈消息,一個或多個消息接受者(消費者)訂閱消息。 從上圖可以看到消息發送者(生產者)和消息接受者(消費者)之間沒有直接耦合,消息發送者將消息發送至分佈式消息隊列即結束對消息的處理,消息接受者從分佈式消息隊列獲取該消息後進行後續處理,並不需要知道該消息從何而來。對新增業務,只要對該類消息感興趣,即可訂閱該消息,對原有系統和業務沒有任何影響,從而實現網站業務的可擴展性設計

消息接受者對消息進行過濾、處理、包裝後,構造成一個新的消息類型,將消息繼續發送出去,等待其他消息接受者訂閱該消息。因此基於事件(消息對象)驅動的業務架構可以是一系列流程。

另外爲了避免消息隊列服務器宕機造成消息丟失,會將成功發送到消息隊列的消息存儲在消息生產者服務器上,等消息真正被消費者服務器處理後才刪除消息。在消息隊列服務器宕機後,生產者服務器會選擇分佈式消息隊列服務器集羣中的其他服務器發佈消息。

備註: 不要認爲消息隊列只能利用發佈-訂閱模式工作,只不過在解耦這個特定業務環境下是使用發佈-訂閱模式的,比如在我們的 ActiveMQ 消息隊列中還有點對點工作模式,具體的會在後面的文章給大家詳細介紹,這一篇文章主要還是讓大家對消息隊列有一個更透徹的瞭解。

這個問題一般會在上一個問題問完之後,緊接着被問到。“使用消息隊列會帶來什麼問題?”這個問題要引起重視,一般我們都會考慮使用消息隊列會帶來的好處而忽略它帶來的問題!

1.2 那麼使用消息隊列會帶來什麼問題?考慮過這些問題嗎?

  • 系統可用性降低: 系統可用性在某種程度上降低,爲什麼這樣說呢?在加入 MQ 之前,你不用考慮消息丟失或者說 MQ 掛掉等等的情況,但是,引入 MQ 之後你就需要去考慮了!
  • 系統複雜性提高: 加入 MQ 之後,你需要保證消息沒有被重複消費、處理消息丟失的情況、保證消息傳遞的順序性等等問題!
  • 一致性問題: 我上面講了消息隊列可以實現異步,消息隊列帶來的異步確實可以提高系統響應速度。但是,萬一消息的真正消費者並沒有正確消費消息怎麼辦?這樣就會導致數據不一致的情況了!

瞭解下面這個問題是爲了我們更好的進行技術選型!該部分摘自:《Java 工程師面試突擊第 1 季-中華石杉老師》,如果大家沒有資源的話,可以在我的公衆號“Java 面試通關手冊”後臺回覆關鍵字“1”即可!

1.3 介紹一下你知道哪幾種消息隊列,該如何選擇呢?

特性 ActiveMQ RabbitMQ RocketMQ Kafka
單機吞吐量 萬級,吞吐量比 RocketMQ 和 Kafka 要低了一個數量級 萬級,吞吐量比 RocketMQ 和 Kafka 要低了一個數量級 10 萬級,RocketMQ 也是可以支撐高吞吐的一種 MQ 10 萬級別,這是 kafka 最大的優點,就是吞吐量高。一般配合大數據類的系統來進行實時數據計算、日誌採集等場景
topic 數量對吞吐量的影響 topic 可以達到幾百,幾千個的級別,吞吐量會有較小幅度的下降這是 RocketMQ 的一大優勢,在同等機器下,可以支撐大量的 topic topic 從幾十個到幾百個的時候,吞吐量會大幅度下降。所以在同等機器下,kafka 儘量保證 topic 數量不要過多。如果要支撐大規模 topic,需要增加更多的機器資源
可用性 高,基於主從架構實現高可用性 高,基於主從架構實現高可用性 非常高,分佈式架構 非常高,kafka 是分佈式的,一個數據多個副本,少數機器宕機,不會丟失數據,不會導致不可用
消息可靠性 有較低的概率丟失數據 經過參數優化配置,可以做到 0 丟失 經過參數優化配置,消息可以做到 0 丟失
時效性 ms 級 微秒級,這是 rabbitmq 的一大特點,延遲是最低的 ms 級 延遲在 ms 級以內
功能支持 MQ 領域的功能極其完備 基於 erlang 開發,所以併發能力很強,性能極其好,延時很低 MQ 功能較爲完善,還是分佈式的,擴展性好 功能較爲簡單,主要支持簡單的 MQ 功能,在大數據領域的實時計算以及日誌採集被大規模使用,是事實上的標準
優劣勢總結 非常成熟,功能強大,在業內大量的公司以及項目中都有應用。偶爾會有較低概率丟失消息,而且現在社區以及國內應用都越來越少,官方社區現在對 ActiveMQ 5.x 維護越來越少,幾個月才發佈一個版本而且確實主要是基於解耦和異步來用的,較少在大規模吞吐的場景中使用 erlang 語言開發,性能極其好,延時很低;吞吐量到萬級,MQ 功能比較完備而且開源提供的管理界面非常棒,用起來很好用。社區相對比較活躍,幾乎每個月都發布幾個版本分在國內一些互聯網公司近幾年用 rabbitmq 也比較多一些但是問題也是顯而易見的,RabbitMQ 確實吞吐量會低一些,這是因爲他做的實現機制比較重。而且 erlang 開發,國內有幾個公司有實力做 erlang 源碼級別的研究和定製?如果說你沒這個實力的話,確實偶爾會有一些問題,你很難去看懂源碼,你公司對這個東西的掌控很弱,基本職能依賴於開源社區的快速維護和修復 bug。而且 rabbitmq 集羣動態擴展會很麻煩,不過這個我覺得還好。其實主要是 erlang 語言本身帶來的問題。很難讀源碼,很難定製和掌控。 接口簡單易用,而且畢竟在阿里大規模應用過,有阿里品牌保障。日處理消息上百億之多,可以做到大規模吞吐,性能也非常好,分佈式擴展也很方便,社區維護還可以,可靠性和可用性都是 ok 的,還可以支撐大規模的 topic 數量,支持複雜 MQ 業務場景。而且一個很大的優勢在於,阿里出品都是 java 系的,我們可以自己閱讀源碼,定製自己公司的 MQ,可以掌控。社區活躍度相對較爲一般,不過也還可以,文檔相對來說簡單一些,然後接口這塊不是按照標準 JMS 規範走的有些系統要遷移需要修改大量代碼。還有就是阿里出臺的技術,你得做好這個技術萬一被拋棄,社區黃掉的風險,那如果你們公司有技術實力我覺得用 RocketMQ 挺好的 kafka 的特點其實很明顯,就是僅僅提供較少的核心功能,但是提供超高的吞吐量,ms 級的延遲,極高的可用性以及可靠性,而且分佈式可以任意擴展。同時 kafka 最好是支撐較少的 topic 數量即可,保證其超高吞吐量。而且 kafka 唯一的一點劣勢是有可能消息重複消費,那麼對數據準確性會造成極其輕微的影響,在大數據領域中以及日誌採集中,這點輕微影響可以忽略這個特性天然適合大數據實時計算以及日誌收集。

這部分內容,我這裏不給出答案,大家可以自行根據自己學習的消息隊列查閱相關內容,我可能會在後面的文章中介紹到這部分內容。另外,下面這些問題在視頻《Java 工程師面試突擊第 1 季-中華石杉老師》中都有提到,如果大家沒有資源的話,可以在我的公衆號“Java 面試通關手冊”後臺回覆關鍵字“1”即可!

1.4 關於消息隊列其他一些常見的問題展望

  1. 引入消息隊列之後如何保證高可用性?
  2. 如何保證消息不被重複消費呢?
  3. 如何保證消息的可靠性傳輸(如何處理消息丟失的問題)?
  4. 我該怎麼保證從消息隊列裏拿到的數據按順序執行?
  5. 如何解決消息隊列的延時以及過期失效問題?消息隊列滿了以後該怎麼處理?有幾百萬消息持續積壓幾小時,說說怎麼解決?
  6. 如果讓你來開發一個消息隊列中間件,你會怎麼設計架構?

2 談談 InnoDB 和 MyIsam 兩者的區別

2.1 兩者的對比

  1. count 運算上的區別: 因爲 MyISAM 緩存有表 meta-data(行數等),因此在做 COUNT(*)時對於一個結構很好的查詢是不需要消耗多少資源的。而對於 InnoDB 來說,則沒有這種緩存
  2. 是否支持事務和崩潰後的安全恢復: MyISAM 強調的是性能,每次查詢具有原子性,其執行速度比 InnoDB 類型更快,但是不提供事務支持。但是 InnoDB 提供事務支持,外部鍵等高級數據庫功能。 具有事務(commit)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)的事務安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外鍵: MyISAM 不支持,而 InnoDB 支持。

2.2 關於兩者的總結

MyISAM 更適合讀密集的表,而 InnoDB 更適合寫密集的表。 在數據庫做主從分離的情況下,經常選擇 MyISAM 作爲主庫的存儲引擎。

一般來說,如果需要事務支持,並且有較高的併發讀取頻率(MyISAM 的表鎖的粒度太大,所以當該表寫併發量較高時,要等待的查詢就會很多了),InnoDB 是不錯的選擇。如果你的數據量很大(MyISAM 支持壓縮特性可以減少磁盤的空間佔用),而且不需要支持事務時,MyISAM 是最好的選擇。

3 聊聊 Java 中的集合吧!

3.1 Arraylist 與 LinkedList 有什麼不同?(注意加上從數據結構分析的內容)

  • 1. 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
  • 2. 底層數據結構: Arraylist 底層使用的是 Object 數組;LinkedList 底層使用的是雙向鏈表數據結構(注意雙向鏈表和雙向循環鏈表的區別:);
  • 3. 插入和刪除是否受元素位置的影響:ArrayList 採用數組存儲,所以插入和刪除元素的時間複雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間複雜度就是 O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就爲 O(n-i)。因爲在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向前移一位的操作。 ② LinkedList 採用鏈表存儲,所以插入,刪除元素時間複雜度不受元素位置的影響,都是近似 O(1) 而數組爲近似 O(n) 。
  • 4. 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)方法)。
  • 5. 內存空間佔用: ArrayList 的空 間浪費主要體現在在 list 列表的結尾會預留一定的容量空間,而 LinkedList 的空間花費則體現在它的每一個元素都需要消耗比 ArrayList 更多的空間(因爲要存放直接後繼和直接前驅以及數據)。

補充內容:RandomAccess 接口

public interface RandomAccess {
}

查看源碼我們發現實際上 RandomAccess 接口中什麼都沒有定義。所以,在我看來 RandomAccess 接口不過是一個標識罷了。標識什麼? 標識實現這個接口的類具有隨機訪問功能。

在 binarySearch() 方法中,它要判斷傳入的 list 是否 RamdomAccess 的實例,如果是,調用 indexedBinarySearch() 方法,如果不是,那麼調用 iteratorBinarySearch() 方法

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArraysList 實現了 RandomAccess 接口, 而 LinkedList 沒有實現。爲什麼呢?我覺得還是和底層數據結構有關!ArraysList 底層是數組,而 LinkedList 底層是鏈表。數組天然支持隨機訪問,時間複雜度爲 O(1) ,所以稱爲快速隨機訪問。鏈表需要遍歷到特定位置才能訪問特定位置的元素,時間複雜度爲 O(n) ,所以不支持快速隨機訪問。,ArraysList 實現了 RandomAccess 接口,就表明了他具有快速隨機訪問功能。 RandomAccess 接口只是標識,並不是說 ArraysList 實現 RandomAccess 接口才具有快速隨機訪問功能的!

下面再總結一下 list 的遍歷方式選擇:

  • 實現了 RandomAccess 接口的 list,優先選擇普通 for 循環 ,其次 foreach,
  • 未實現 RandomAccess 接口的 ist, 優先選擇 iterator 遍歷(foreach 遍歷底層也是通過 iterator 實現的),大 size 的數據,千萬不要使用普通 for 循環

Java 中的集合這類問題幾乎是面試必問的,問到這類問題的時候,HashMap 又是幾乎必問的問題,所以大家一定要引起重視!

3.2 HashMap 的底層實現

1)JDK1.8 之前

JDK1.8 之前 HashMap 底層是 數組和鏈表 結合在一起使用也就是 鏈表散列HashMap 通過 key 的 hashCode 經過擾動函數處理過後得到 hash 值,然後通過 (n - 1) & hash 判斷當前元素存放的位置(這裏的 n 指的時數組的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鍊法解決衝突。

所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是爲了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數之後可以減少碰撞。

JDK 1.8 HashMap 的 hash 方法源碼:

JDK 1.8 的 hash 方法 相比於 JDK 1.7 hash 方法更加簡化,但是原理不變。

      static final int hash(Object key) {
        int h;
        // key.hashCode():返回散列值也就是hashcode
        // ^ :按位異或
        // >>>:無符號右移,忽略符號位,空位都以0補齊
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

對比一下 JDK1.7 的 HashMap 的 hash 方法源碼.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

相比於 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會稍差一點點,因爲畢竟擾動了 4 次。

所謂 “拉鍊法” 就是:將鏈表和數組相結合。也就是說創建一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希衝突,則將衝突的值加到鏈表中即可。

jdk1.8之前的內部結構-HashMap

2)JDK1.8 之後

相比於之前的版本, JDK1.8 之後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲 8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。

jdk1.8之後的內部結構-HashMap

TreeMap、TreeSet 以及 JDK1.8 之後的 HashMap 底層都用到了紅黑樹。紅黑樹就是爲了解決二叉查找樹的缺陷,因爲二叉查找樹在某些情況下會退化成一個線性結構。

問完 HashMap 的底層原理之後,面試官可能就會緊接着問你 HashMap 底層數據結構相關的問題!

3.3 既然談到了紅黑樹,你給我手繪一個出來吧,然後簡單講一下自己對於紅黑樹的理解

紅黑樹

紅黑樹特點:

  1. 每個節點非紅即黑;
  2. 根節點總是黑色的;
  3. 每個葉子節點都是黑色的空節點(NIL 節點);
  4. 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定);
  5. 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

紅黑樹的應用:

TreeMap、TreeSet 以及 JDK1.8 之後的 HashMap 底層都用到了紅黑樹。

爲什麼要用紅黑樹

簡單來說紅黑樹就是爲了解決二叉查找樹的缺陷,因爲二叉查找樹在某些情況下會退化成一個線性結構。

3.4 紅黑樹這麼優秀,爲何不直接使用紅黑樹得了?

說一下自己對於這個問題的看法:我們知道紅黑樹屬於(自)平衡二叉樹,但是爲了保持“平衡”是需要付出代價的,紅黑樹在插入新數據後可能需要通過左旋,右旋、變色這些操作來保持平衡,這費事啊。你說說我們引入紅黑樹就是爲了查找數據快,如果鏈表長度很短的話,根本不需要引入紅黑樹的,你引入之後還要付出代價維持它的平衡。但是鏈表過長就不一樣了。至於爲什麼選 8 這個值呢?通過概率統計所得,這個值是綜合查詢成本和新增元素成本得出的最好的一個值。

3.5 HashMap 和 Hashtable 的區別/HashSet 和 HashMap 區別

HashMap 和 Hashtable 的區別

  1. 線程是否安全: HashMap 是非線程安全的,Hashtable 是線程安全的;Hashtable 內部的方法基本都經過 synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);
  2. 效率: 因爲線程安全的問題,HashMap 要比 Hashtable 效率高一點。另外,Hashtable 基本被淘汰,不要在代碼中使用它;
  3. 對 Null key 和 Null value 的支持: HashMap 中,null 可以作爲鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值爲 null。但是在 Hashtable 中 put 進的鍵值只要有一個 null,直接拋出 NullPointerException。
  4. 初始容量大小和每次擴充容量大小的不同 : ① 創建時如果不指定容量初始值,Hashtable 默認的初始大小爲 11,之後每次擴充,容量變爲原來的 2n+1。HashMap 默認的初始化大小爲 16。之後每次擴充,容量變爲原來的 2 倍。② 創建時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充爲 2 的冪次方大小(HashMap 中的tableSizeFor()方法保證,下面給出了源代碼)。也就是說 HashMap 總是使用 2 的冪作爲哈希表的大小,後面會介紹到爲什麼是 2 的冪次方。
  5. 底層數據結構: JDK1.8 以後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲 8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。

HashSet 和 HashMap 區別

如果你看過 HashSet 源碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的源碼非常非常少,因爲除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。)

HashSet 和 HashMap 區別

三 終結篇

1. Object 類有哪些方法?

這個問題,面試中經常出現。我覺得不論是出於應付面試還是說更好地掌握 Java 這門編程語言,大家都要掌握!

1.1 Object 類的常見方法總結

Object 類是一個特殊的類,是所有類的父類。它主要提供了以下 11 個方法:


public final native Class<?> getClass()//native方法,用於返回當前運行時對象的Class對象,使用了final關鍵字修飾,故不允許子類重寫。

public native int hashCode() //native方法,用於返回對象的哈希碼,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用於比較2個對象的內存地址是否相等,String類對該方法進行了重寫用戶比較字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用於創建並返回當前對象的一份拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 爲true,x.clone().getClass() == x.getClass() 爲true。Object本身沒有實現Cloneable接口,所以不重寫clone方法並且進行調用的話會發生CloneNotSupportedException異常。

public String toString()//返回類的名字@實例的哈希碼的16進制的字符串。建議Object所有的子類都重寫這個方法。

public final native void notify()//native方法,並且不能重寫。喚醒一個在此對象監視器上等待的線程(監視器相當於就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。

public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此對象監視器上等待的所有線程,而不是一個線程。

public final native void wait(long timeout) throws InterruptedException//native方法,並且不能重寫。暫停線程的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos參數,這個參數表示額外時間(以毫微秒爲單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念

protected void finalize() throws Throwable { }//實例被垃圾回收器回收的時候觸發的操作

問完上面這個問題之後,面試官很可能緊接着就會問你“hashCode 與 equals”相關的問題。

1.2 hashCode 與 equals

面試官可能會問你:“你重寫過 hashcode 和 equals 麼,爲什麼重寫 equals 時必須重寫 hashCode 方法?”

1.2.1 hashCode()介紹

hashCode() 的作用是獲取哈希碼,也稱爲散列碼;它實際上是返回一個 int 整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在 JDK 的 Object.java 中,這就意味着 Java 中的任何類都包含有 hashCode() 函數。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法通常用來將對象的 內存地址 轉換爲整數之後返回。

    public native int hashCode();

散列表存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用到了散列碼!(可以快速找到所需要的對象)

1.2.2 爲什麼要有 hashCode

我們以“HashSet 如何檢查重複”爲例子來說明爲什麼要有 hashCode:

當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設對象沒有重複出現。但是如果發現有相同 hashcode 值的對象,這時會調用 equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。(摘自我的 Java 啓蒙書《Head fist java》第二版)。這樣我們就大大減少了 equals 的次數,相應就大大提高了執行速度。

1.2.3 hashCode()與 equals()的相關規定

  1. 如果兩個對象相等,則 hashcode 一定也是相同的
  2. 兩個對象相等,對兩個對象分別調用 equals 方法都返回 true
  3. 兩個對象有相同的 hashcode 值,它們也不一定是相等的
  4. 因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
  5. hashCode()的默認行爲是對堆上的對象產生獨特值。如果沒有重寫 hashCode(),則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)

1.2.4 爲什麼兩個對象有相同的 hashcode 值,它們也不一定是相等的?

在這裏解釋一位小夥伴的問題。以下內容摘自《Head Fisrt Java》。

因爲 hashCode() 所使用的雜湊算法也許剛好會讓多個對象傳回相同的雜湊值。越糟糕的雜湊算法越容易碰撞,但這也與數據值域分佈的特性有關(所謂碰撞也就是指的是不同的對象得到相同的 hashCode)。

我們剛剛也提到了 HashSet,如果 HashSet 在對比的時候,同樣的 hashcode 有多個對象,它會使用 equals() 來判斷是否真的相同。也就是說 hashcode 只是用來縮小查找成本。

==與 equals 的對比也是比較常問的基礎問題之一!

1.3 ==與 equals

== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象。(基本數據類型比較的是值,引用數據類型比較的是內存地址)

equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:

  • 情況 1:類沒有覆蓋 equals()方法。則通過 equals()比較該類的兩個對象時,等價於通過“==”比較這兩個對象。
  • 情況 2:類覆蓋了 equals()方法。一般,我們都覆蓋 equals()方法來兩個對象的內容相等;若它們的內容相等,則返回 true(即,認爲這兩個對象相等)。

舉個例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 爲一個引用
        String b = new String("ab"); // b爲另一個引用,對象的內容一樣
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 從常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一對象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}

說明:

  • String 中的 equals()方法是被重寫過的,因爲 Object 的 equals()方法是比較的對象的內存地址,而 String 的 equals()方法比較的是對象的值。
  • 當創建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經存在的值和要創建的值相同的對象,如果有就把它賦給當前引用。如果沒有就在常量池中重新創建一個 String 對象。

【備戰春招/秋招系列 5】美團面經總結進階篇 (附詳解答案) 這篇文章中,我們已經提到了一下關於 HashMap 在面試中常見的問題:HashMap 的底層實現、簡單講一下自己對於紅黑樹的理解、紅黑樹這麼優秀,爲何不直接使用紅黑樹得了、HashMap 和 Hashtable 的區別/HashSet 和 HashMap 區別。HashMap 和 ConcurrentHashMap 這倆兄弟在一般只要面試中問到集合相關的問題就一定會被問到,所以各位務必引起重視!

2 ConcurrentHashMap 相關問題

2.1 ConcurrentHashMap 和 Hashtable 的區別

ConcurrentHashMap 和 Hashtable 的區別主要體現在實現線程安全的方式上不同。

  • 底層數據結構: JDK1.7 的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟 HashMap1.8 的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
  • 實現線程安全的方式(重要):在 JDK1.7 的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。(默認分配 16 個 Segment,比 Hashtable 效率提高 16 倍。) 到了 JDK1.8 的時候已經摒棄了 Segment 的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6 以後 對 synchronized 鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖):使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

兩者的對比圖:

圖片來源:http://www.cnblogs.com/chengxiao/p/6842045.html

Hashtable:

JDK1.7 的 ConcurrentHashMap:

JDK1.8 的 ConcurrentHashMap(TreeBin: 紅黑二叉樹節點
Node: 鏈表節點):

2.2 ConcurrentHashMap 線程安全的具體實現方式/底層具體實現

JDK1.7(上面有示意圖)

首先將數據分爲一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成

Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和 HashMap 類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護着一個 HashEntry 數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 的鎖。

JDK1.8(上面有示意圖)

ConcurrentHashMap 取消了 Segment 分段鎖,採用 CAS 和 synchronized 來保證併發安全。數據結構跟 HashMap1.8 的結構類似,數組+鏈表/紅黑二叉樹。

synchronized 只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要 hash 不衝突,就不會產生併發,效率又提升 N 倍。

3 談談 synchronized 和 ReentrantLock 的區別

① 兩者都是可重入鎖

兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增 1,所以要等到鎖的計數器下降爲 0 時才能釋放鎖。

② synchronized 依賴於 JVM 而 ReentrantLock 依賴於 API

synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。

③ ReentrantLock 比 synchronized 增加了一些高級功能

相比 synchronized,ReentrantLock 增加了一些高級功能。主要來說主要有三點:① 等待可中斷;② 可實現公平鎖;③ 可實現選擇性通知(鎖可以綁定多個條件)

  • ReentrantLock 提供了一種能夠中斷等待鎖的線程的機制,通過 lock.lockInterruptibly() 來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改爲處理其他事情。
  • ReentrantLock 可以指定是公平鎖還是非公平鎖。而 synchronized 只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock 默認情況是非公平的,可以通過 ReentrantLock 類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
  • synchronized 關鍵字與 wait()和 notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock 類當然也可以實現,但是需要藉助於 Condition 接口與 newCondition() 方法。Condition 是 JDK1.5 之後纔有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個 Lock 對象中可以創建多個 Condition 實例(即對象監視器),線程對象可以註冊在指定的 Condition 中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用 notify/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用 ReentrantLock 類結合 Condition 實例可以實現“選擇性通知” ,這個功能非常重要,而且是 Condition 接口默認提供的。而 synchronized 關鍵字就相當於整個 Lock 對象中只有一個 Condition 實例,所有的線程都註冊在它一個身上。如果執行 notifyAll()方法的話就會通知所有處於等待狀態的線程這樣會造成很大的效率問題,而 Condition 實例的 signalAll()方法 只會喚醒註冊在該 Condition 實例中的所有等待線程。

如果你想使用上述功能,那麼選擇 ReentrantLock 是一個不錯的選擇。

④ 兩者的性能已經相差無幾

在 JDK1.6 之前,synchronized 的性能是比 ReentrantLock 差很多。具體表示爲:synchronized 關鍵字吞吐量隨線程數的增加,下降得非常嚴重。而 ReentrantLock 基本保持一個比較穩定的水平。我覺得這也側面反映了, synchronized 關鍵字還有非常大的優化餘地。後續的技術發展也證明了這一點,我們上面也講了在 JDK1.6 之後 JVM 團隊對 synchronized 關鍵字做了很多優化。JDK1.6 之後,synchronized 和 ReentrantLock 的性能基本是持平了。所以網上那些說因爲性能才選擇 ReentrantLock 的文章都是錯的!JDK1.6 之後,性能已經不是選擇 synchronized 和 ReentrantLock 的影響因素了!而且虛擬機在未來的性能改進中會更偏向於原生的 synchronized,所以還是提倡在 synchronized 能滿足你的需求的情況下,優先考慮使用 synchronized 關鍵字來進行同步!優化後的 synchronized 和 ReentrantLock 一樣,在很多地方都是用到了 CAS 操作。

4 線程池瞭解嗎?

4.1 爲什麼要用線程池?

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java 併發編程的藝術》提到的來說一下使用線程池的好處:

  • 降低資源消耗。 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。 當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

4.2 Java 提供了哪幾種線程池?他們各自的使用場景是什麼?

Java 主要提供了下面 4 種線程池

  • FixedThreadPool: 該方法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池中若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閒時,便處理在任務隊列中的任務。
  • SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。
  • CachedThreadPool: 該方法返回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務執行完畢後,將返回線程池進行復用。
  • ScheduledThreadPoolExecutor: 主要用來在給定的延遲後運行任務,或者定期執行任務。ScheduledThreadPoolExecutor 又分爲:ScheduledThreadPoolExecutor(包含多個線程)和 SingleThreadScheduledExecutor (只包含一個線程)兩種。

各種線程池的適用場景介紹

  • FixedThreadPool: 適用於爲了滿足資源管理需求,而需要限制當前線程數量的應用場景。它適用於負載比較重的服務器;
  • SingleThreadExecutor: 適用於需要保證順序地執行各個任務並且在任意時間點,不會有多個線程是活動的應用場景;
  • CachedThreadPool: 適用於執行很多的短期異步任務的小程序,或者是負載較輕的服務器;
  • ScheduledThreadPoolExecutor: 適用於需要多個後臺執行週期任務,同時爲了滿足資源管理需求而需要限制後臺線程的數量的應用場景;
  • SingleThreadScheduledExecutor: 適用於需要單個後臺線程執行週期任務,同時保證順序地執行各個任務的應用場景。

4.3 創建的線程池的方式

(1) 使用 Executors 創建

我們上面剛剛提到了 Java 提供的幾種線程池,通過 Executors 工具類我們可以很輕鬆的創建我們上面說的幾種線程池。但是實際上我們一般都不是直接使用 Java 提供好的線程池,另外在《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

Executors 返回線程池對象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而導致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致OOM。

(2) ThreadPoolExecutor 的構造函數創建

我們可以自己直接調用 ThreadPoolExecutor 的構造函數來自己創建線程池。在創建的同時,給 BlockQueue 指定容量就可以了。示例如下:

private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(13));

這種情況下,一旦提交的線程數超過當前可用線程數時,就會拋出 java.util.concurrent.RejectedExecutionException,這是因爲當前線程池使用的隊列是有邊界隊列,隊列已經滿了便無法繼續處理新的請求。但是異常(Exception)總比發生錯誤(Error)要好。

(3) 使用開源類庫

Hollis 大佬之前在他的文章中也提到了:“除了自己定義 ThreadPoolExecutor 外。還有其他方法。這個時候第一時間就應該想到開源類庫,如 apache 和 guava 等。”他推薦使用 guava 提供的 ThreadFactoryBuilder 來創建線程池。下面是參考他的代碼示例:

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

通過上述方式創建線程時,不僅可以避免 OOM 的問題,還可以自定義線程名稱,更加方便的出錯的時候溯源。

5 Nginx

5.1 簡單介紹一下 Nginx

Nginx 是一款輕量級的 Web 服務器/反向代理服務器及電子郵件(IMAP/POP3)代理服務器。 Nginx 主要提供反向代理、負載均衡、動靜分離(靜態資源服務)等服務。下面我簡單地介紹一下這些名詞。

反向代理

談到反向代理,就不得不提一下正向代理。無論是正向代理,還是反向代理,說到底,就是代理模式的衍生版本罷了

  • **正向代理:**某些情況下,代理我們用戶去訪問服務器,需要用戶手動的設置代理服務器的 ip 和端口號。正向代理比較常見的一個例子就是 VPN 了。
  • 反向代理: 是用來代理服務器的,代理我們要訪問的目標服務器。代理服務器接受請求,然後將請求轉發給內部網絡的服務器,並將從服務器上得到的結果返回給客戶端,此時代理服務器對外就表現爲一個服務器。

通過下面兩幅圖,大家應該更好理解(圖源:http://blog.720ui.com/2016/nginx_action_05_proxy/):

正向代理

反向代理

所以,簡單的理解,就是正向代理是爲客戶端做代理,代替客戶端去訪問服務器,而反向代理是爲服務器做代理,代替服務器接受客戶端請求。

負載均衡

在高併發情況下需要使用,其原理就是將併發請求分攤到多個服務器執行,減輕每臺服務器的壓力,多臺服務器(集羣)共同完成工作任務,從而提高了數據的吞吐量。

Nginx 支持的 weight 輪詢(默認)、ip_hash、fair、url_hash 這四種負載均衡調度算法,感興趣的可以自行查閱。

負載均衡相比於反向代理更側重的是將請求分擔到多臺服務器上去,所以談論負載均衡只有在提供某服務的服務器大於兩臺時纔有意義。

動靜分離

動靜分離是讓動態網站裏的動態網頁根據一定規則把不變的資源和經常變的資源區分開來,動靜資源做好了拆分以後,我們就可以根據靜態資源的特點將其做緩存操作,這就是網站靜態化處理的核心思路。

5.2 爲什麼要用 Nginx?

這部分內容參考極客時間—Nginx 核心知識 100 講的內容

如果面試官問你這個問題,就一定想看你知道 Nginx 服務器的一些優點嗎。

Nginx 有以下 5 個優點:

  1. 高併發、高性能(這是其他 web 服務器不具有的)
  2. 可擴展性好(模塊化設計,第三方插件生態圈豐富)
  3. 高可靠性(可以在服務器行持續不間斷的運行數年)
  4. 熱部署(這個功能對於 Nginx 來說特別重要,熱部署指可以在不停止 Nginx 服務的情況下升級 Nginx)
  5. BSD 許可證(意味着我們可以將源代碼下載下來進行修改然後使用自己的版本)

5.3 Nginx 的四個主要組成部分了解嗎?

這部分內容參考極客時間—Nginx 核心知識 100 講的內容

  • Nginx 二進制可執行文件:由各模塊源碼編譯出一個文件
  • nginx.conf 配置文件:控制 Nginx 行爲
  • acess.log 訪問日誌: 記錄每一條 HTTP 請求信息
  • error.log 錯誤日誌:定位問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章