美團面試常見問題總結

原文鏈接:https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/PreparingForInterview/%E7%BE%8E%E5%9B%A2%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.md

原文鏈接:  https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/PreparingForInterview/%E7%BE%8E%E5%9B%A2%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.md

版權原作者所有,此處個人記錄備用

一 基礎篇

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 可以通過HttpServletRequest 的 getRequestDispatcher()方法獲得。例如下面的代碼就是跳轉到 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. 連接結束

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

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

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

參考文章https://blog.csdn.net/qq_38950316/article/details/81087809

  • 客戶端–發送帶有 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

https://blog.csdn.net/qq_38950316/article/details/81087809

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 停頓時間。

參考文章:https://blog.csdn.net/w13707470416/article/details/85319981
餓漢式(靜態變量)

public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();
    private Singleton1 (){}
    //靜態工廠
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

餓漢式就是在類加載初始化的時候就創建了對象,儘管你還不需要使用他。相對來說我是比較喜歡這種方式的,既沒有線程安全,速度也快,當然這是以犧牲空間換取速度,大家可以根據實際情況,根據該對象被創建的概率酌情使用該方法。

餓漢式(靜態代碼塊

public class Singleton2 { 
   private static Singleton2 instance = null;
   private Singleton2 (){}
    //靜態代碼塊 
   static { 
       instance = new Singleton2();
   }

   public Singleton2 getInstance(){ 
       return instance;
   }
}

靜態代碼塊實現的餓漢式與上述第一種方式類似,都是在類初始化的時候就創建對象,同樣是線程安全的,其實就是JVM幫我們避免了線程安全,因爲類只要加載一次,所以只會創建一個對象。

懶漢式(多線程下不可用)


public class Singleton3 {
    private static Singleton3 instance = null;
    private Singleton3 (){}
    public static Singleton3 getInstance(){
        if (null == instance){       //(1)
            instance = new Singleton3();   //(2)
        }
        return instance;
    }
}

 這個懶漢式寫法是線程不安全的,只能在單線程情況下使用。假設有兩個線程A和B,線程A運行到(1)行代碼時時間片已經完了,此時CPU切換執行線程B,線程B完成整個流程,也就是說這個時候instance已經不爲空了,然後再執行線程A,此時線程A就會執行(2)行語句重新創建新的實例。

懶漢式(多線程下可用,但效率低)


public class Singleton4 {
    private static Singleton4 instance = null;
    private Singleton4 (){}
    public static synchronized Singleton4 getInstance() {
        if (null == instance){
            instance = new Singleton4();
        }
        return instance;
    }
}

第四個方法與第三個方法的區別在於在getInstance方法中加了synchronized同步鎖,這樣會導致只有等一個線程運行完後另一個線程才能進入該方法,當然不會有線程安全的問題,但我們只是在一開始實例化對象時需要,之後只需要直接返回對象即可,這樣相當於每次都要去查詢對象是否實例化,且還要排隊查詢,這樣顯然很影響性能,相當於每個線程都要排隊執行該方法。所以不推薦使用這種寫法。

靜態內部類方式

靜態內部實現的單例是懶加載的且線程安全。只有通過顯式調用 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;  
    }  
} 

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

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;
    }
}

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 實現原理

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

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

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

特性 ActiveMQ RabbitMQ RocketMQ Kafaka
單機吞吐量 萬級,吞吐量比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唯一的一點劣勢是有可能消息重複消費,那麼對數據準確性會造成極其輕微的影響,在大數據領域中以及日誌採集中,這點輕微影響可以忽略這個特性天然適合大數據實時計算以及日誌收集。

15.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更多的空間(因爲要存放直接後繼和直接前驅以及數據)。

16.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 沒有這樣的機制。

17.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,競爭會越來越激烈效率越低。

18.談談 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操作。

19.爲什麼要用線程池?

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

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

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

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

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

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

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

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

21.創建的線程池的方式

(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的問題,還可以自定義線程名稱,更加方便的出錯的時候溯源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章