騰訊一面面試復盤


1、HTTP, TCP, UDP 的區別?

首先要講一下HTTP和TCP的區別;然后將一下TCP和UDP的區別。

TCP和UDP的區別已經復習過了,然后重點了解一下HTTP和TCP的區別。

TCP和UDP的區別

TCP 是⾯向連接的、可靠的、基於字節流的傳輸層通信協議。UDP是面向報文的,無連接的通信協議

1、TCP是可靠的交付數據,數據無差錯,不丟失不重復,按需到達。UDP是不可靠的,盡最大努力交付;

2、TCP是一對一的點服務,端到端的通信。UDP是可以一對一,一對多。多對多的交互通信;

3、TCP的使用場景是FTP文件傳輸,HTTP,HTTPS ;UDP適用於音視頻多媒體通信,廣播通信。

4、TCP的首部長度較長,沒有使用的話就有20個字節。使用了以后更加地長。UDP的首部只有8個字節;

5、TCP有流量控制,擁塞控制,保證數據的傳輸的安全性;UDP沒有擁塞控制,即使網絡擁塞,也不會影響UDP的發送效率。

6、TCP是面向字節流的,無邊界,順序可靠;UDP是面向報文的,一個包一個包地發送,容易丟包亂序。

HTTP和TCP的區別

連接來看,TCP連接需要三次握手,握手過程中,傳送的包不包含任何數據,三次握手完成后,雙方開始傳送數據。HTTP的一個特點是客戶端發送的每次請求都需要服務器回送響應。在請求結束后,會主動釋放連接。從建立連接到關閉連接的過程稱為“一次連接”.

HTTP1.0 ,客戶端的每次請求都要建立一次單獨的連接,處理完本次請求之后,自動釋放連接。HTTP1.1中,一次連接可以處理多個請求,並且多個請求可以連續發出去。不必等待一個請求結束后再發送另一個請求。也就是長連接,使用keep-alive來實現的。總而言之,HTTP連接使用的是請求-響應的方式,不僅要在請求的時候建立連接,而且還要在客戶端向服務端發送了一個請求之后,服務端才能響應數據。

Socket連接

socket(套接字的概念)就是應用程序通過傳輸層進行數據通信時,TCP通常會遇到為多個應用程序進程提供並發服務的問題。
多個TCP連接和多個應用程序進程可能需要同一個TCP協議端口進行通信。所以,為了區分不同的應用程序進程和連接,操作系統提供了socket接口。應用層可以通過socket接口,區分來自不同應用程序進程或網絡連接的通信,實現數據傳輸的並發服務。

2、數組和鏈表的區別?

數組是一組具有相同數據類型的變量的集合,這些變量稱之為集合的元素。

鏈表是一種物理存儲單元上非連續、非順序的存儲結構, 數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。

1、數組的長度是固定的,容易角標越界。鏈表的長度是可變的;

2、數組的內存空間是連續的,鏈表的內存空間是不連續的。

3、數組插入和刪除一個元素的時間復雜度是 o(N),鏈表是o(1);

4、數組查詢一個元素,調用get()通過下標訪問,訪問效率高;但是鏈表查詢一個元素要從頭遍歷,時間復雜度是o(n)

3、TCP的流量控制

1 、為什么要進行流量控制?

雙方在通信的時候,發送方的速率與接收方的速率不一定相等,如果發送方的速率太快,會導致接收方處理不過來。這個時候,接收方只能把處理不過來的數據存在緩存里(失序的數據包也會被存放在緩存區里)。

如果緩存區滿了,發送方還在發送數據,那么接收方就只能把接收的數據包丟掉,大量的丟包會極大地浪費網絡資源。因此,我們需要控制發送方的發送速率,讓接收方與發送方處於一種動態平衡才好。

所以,對發送方發送速率進行控制就做流量控制。

2、如何控制?

接收方每次收到數據包,可以在發送確認報文的時候告知自己的緩存區還剩多少是空閑的。我們也把緩存區的剩余大小稱之為接收窗口的大小。用win來表示
。發送方收到之后,便會調整自己的發送速率,也就是調整自己發送窗口的大小,當發送方收到接收窗口的大小為0時,發送方就會停止發送數據,防止出現大量丟包情況發生。

3、發送方何時再繼續發送數據?

當發送方停止發送數據后,該怎么才能知道自己可以繼續發送數據?

我們可以采用這樣的策略,當接收方處理好數據,接受窗口win>0 時,接收方發個通知去通知對方,告訴他可以繼續發送數據了。當發送方收到窗口大於0的報文時,就繼續發送數據。但是如果由於網絡原因,接收端發送的通知報文由於某種原因,這個報文丟失了,這時候就會引發一個問題。接收方發了通知報文后
,繼續等待發送方發送數據,而發送發則在等待接收方的通知報文,此時雙方就會陷入一種僵局。

為了解決該問題,我們采用了另外一種策略:當發送方收到接收窗口win =0 時,這時發送方停止發送報文,並且同時開啟一個定時器。每隔一段時間就發測試報文去詢問對方,打聽是否可以發送數據了。如果可以,接收方就告訴他此時接收窗口的大小;如果接收窗口大小還是為0 ,則發送方再次刷新啟動定時器。

需要注意的是,通信雙方都有兩個滑動窗口。一個用於接收數據,一個用於發送數據(發送窗口)。指出接收窗口大小的通知為窗口通告。接受窗口如果太小的話,顯然這是不行的,這會嚴重浪費鏈路利用率,增加丟包率。那是否越大越好呢?

答否,當接收窗口達到某個值的時候,再增大的話也不怎么會減少丟包率的了,而且還會更加消耗內存。所以接收窗口的大小必須根據網絡環境以及發送發的的擁塞窗口來動態調整。

接收方在發送確認報文的時候,會告訴發送方自己的接收窗口大小,而發送方的發送窗口會據此來設置自己的發送窗口,但這並不意味着他們就會相等。首先接收方把確認報文發出去的那一刻,就已經在一邊處理堆在自己緩存區的數據了,所以一般情況下接收窗口 >= 發送窗口。

4、TCP三次握手,為什么握手是三次,揮手是四次?網絡中斷了會怎么樣?

這是因為服務端在監聽狀態,收到建立連接的報文之后,把ACK和SYN放在一個報文里發送給客戶端。而關閉連接時,服務端收到客戶端斷開的FIN報文,僅僅表示客戶端已經沒有數據要發送過來了,但是服務端可能還有一些數據要發送給客戶端。所以服務端這邊在收到一個FIN報文后,先回應一個ACK表示收到。然后等待發送完需要發送出去的數據后,再發送給客戶端一個FIN報文,表示同意現在關閉連接。因此,服務端方ACK和FIN一般都會分開發送。

網絡斷開了會怎么樣?對於tcp連接,如果一直在socket上有數據來往就不會觸發keepalive,但是如果30秒一直沒有數據往來,則keep-alive開始工作:發送探測包,受到響應則認為網絡,是好的,結束探測;如果沒有相應就每隔1秒發探測包,一共發送3次,3次后仍沒有相應,就關閉連接,也就是從網絡開始斷開到你的socket能夠意識到網絡異常,最多花33秒。

Keep-Alive模式是為了讓HTTP保持連接的狀態。也就是客戶端到服務端的連接持續有效。如果沒有開啟keep-alive模式,那么TCP在請求應答結束之后就會斷
開連接,因為HTTP協議是無連接的協議。

5、線程池如何講?

1、 為什么需要線程池?

Java中為了提高並發度,可以使用多線程共同執行,但是如果有大量的線程短時間之內被創建或者銷毀,會占用大量的系統時間,影響系統效率。為了解決這個問題,java中引入線程池,可以使創建好的線程在指定的時間內由系統統一管理,而不是在執行時創建,執行后就銷毀。從而避免了頻繁創建,銷毀線程帶來的開銷。

2、 線程池的處理流程?

Java中實現多線程有兩種途徑:繼承Thread類或者Runnable接口。當我們把Runnable交給線程池去執行的時候,這個線程池處理的流程是:提交任務,判斷核心線程是否已滿?如果沒有滿,創建線程執行任務。如果核心線程已滿,那么看看工作隊列是否已滿,如果工作隊列沒有滿,將任務放在工作隊列,如果工作隊列滿了,看看當前線程是否達到最大線程數?如果沒有到達最大線程數,創建線程執行任務,如果達到了最大線程數,就按拒絕策略處理任務。

3、 線程池的參數

在java中,線程池的概念是Executor接口,具體實現為ThreadPoolExecutor類。

ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器:

public class ThreadPoolExecutor extends AbstractExecutorService {

…..

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue);

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

…

}

newCachedThreadPool:

底層:返回ThreadPoolExecutor實例,corePoolSize為0;maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為60L;unit為TimeUnit.SECONDS;workQueue為SynchronousQueue(同步隊列)
通俗:當有新任務到來,則插入到SynchronousQueue中,由於SynchronousQueue是同步隊列,因此會在池中尋找可用線程來執行,若有可以線程則執行,若沒有可用線程則創建一個線程來執行該任務;若池中線程空閑時間超過指定大小,則該線程會被銷毀。
適用:執行很多短期異步的小程序或者負載較輕的服務器
newFixedThreadPool:

底層:返回ThreadPoolExecutor實例,接收參數為所設定線程數量nThread,corePoolSize為nThread,maximumPoolSize為nThread;keepAliveTime為0L(不限時);unit為:TimeUnit.MILLISECONDS;WorkQueue為:new LinkedBlockingQueue () 無解阻塞隊列
通俗:創建可容納固定數量線程的池子,每隔線程的存活時間是無限的,當池子滿了就不在添加線程了;如果池中的所有線程均在繁忙狀態,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
適用:執行長期的任務,性能好很多
newSingleThreadExecutor:

底層:FinalizableDelegatedExecutorService包裝的ThreadPoolExecutor實例,corePoolSize為1;maximumPoolSize為1;keepAliveTime為0L;unit為:TimeUnit.MILLISECONDS;workQueue為:new LinkedBlockingQueue () 無解阻塞隊列
通俗:創建只有一個線程的線程池,且線程的存活時間是無限的;當該線程正繁忙時,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
適用:一個任務一個任務執行的場景
NewScheduledThreadPool:

底層:創建ScheduledThreadPoolExecutor實例,corePoolSize為傳遞來的參數,maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為0;unit為:TimeUnit.NANOSECONDS;workQueue為:new DelayedWorkQueue() 一個按超時時間升序排序的隊列
通俗:創建一個固定大小的線程池,線程池內線程存活時間無限制,線程池可以支持定時及周期性任務執行,如果所有線程均處於繁忙狀態,對於新任務會進入DelayedWorkQueue隊列中,這是一種按照超時時間排序的隊列結構
適用:周期性執行任務的場景
線程池的基本大小:corePoolSize。當提交一個任務時,線程池會創建一個線程來執行任務。即使其他的空間的基本線程能夠執行新任務也會創建線程。等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啟動所有基本線程。

任務隊列:workQueue。用於保存等待執行任務的阻塞隊列。可以選擇以下幾個阻塞隊列。

ArrayBlockingQueue :是一個基於數組結構的有界阻塞隊列,此隊列按先進先出原則對元素進行排序。

LinkedBlockingQueue :是一個基於鏈表結構的阻塞隊列,此隊列按FIFO(先進先出)排序元素,吞吐量通常要高於ArrayBlockingQueue。

SynchronousQueue : 一個不存儲元素的阻塞隊列。每個插入操作必須等待另一個線程調用移除操作。否則插入操作一直處於阻塞狀態,吞吐量高於LinkedBlockingQueue。

PriorityBlockingQueue : 一個具有優先級的無限阻塞隊列。

線程池的最大線程數目:MaxiMumPoolSize: 。線程池允許創建的最大線程數。如果隊列滿了,那么就會再創建新的線程執行任務。但是不能超過最大線程
數。否則執行拒絕策略。

拒絕策略
1、只用調用者所在的線程來運行任務。

2、丟棄掉隊列里最近的一個任務,並執行當前任務。

3、不處理,丟棄掉。

KeepAliveTime: 線程池的工作線程空閑后,保持存活的時間。

4、 線程池的創建

New ThreadPoolExecutor()的方式創建線程池。

6、HashMap的底層原理?

1、 jdk1.8,講一下put()的底層原理。

開始,首先判斷數組是否為空?如果數組為空。那么初始化數組。如果數組不為空,根據哈希算法計算key在數組中的存儲位置,如果計算得到的指定位置不存在數據,那么存放節點,如果存在數據,說明發生了Hash沖突。然后計算key與當前位置的key的哈希值是否相等,如果相等,那么覆蓋舊的value值。如果哈希值不相等,判斷當前節點是否是紅黑樹的節點,如果不是紅黑樹節點,那么加入鏈表。如果是紅黑樹節點,放入紅黑樹中。

加入鏈表以后,還得去判斷鏈表的節點數是否大於8 ,大於8則轉化為紅黑樹。最后放好節點以后,再去判斷當前數組中元素的個數超過閾值沒?超過0.75 * n時,將擴容為2*n的大小,遷移數據。結束。

2、 jdk1.8在jdk1.7上做的三點優化?

(1、整體結構轉化為了數組 + 鏈表 + 紅黑樹。

(2、元素插入鏈表時 ,由原先的頭插法變成了尾插法。

(3、1.7擴容遷移數據時,進行rehash計算在新數組的位置。1.8的時候,不用rehash, 原始元素索引不變,容量增大為原來的2倍。

3、什么時候resize()?

當元素個數超過數組大小loader時,會自動擴容。比如原始大小為16,那么在160.75= 12的時候就擴容。這也是為了性能考慮才這樣設計。

4、HashMap的擴容因子為啥是0.75?

為了提高空間利用率,減少查詢成本的一個折中方案。因為泊松分布在0.75的時候碰撞最小。加載因子是哈希表在其容量自動擴容之前可以達到多滿的一種度量。當哈希表的條目數超過加載因子與當前容量的乘積時,則要進行擴容,rehash(重建內部數據結構),擴容后的哈希表將具有2倍的原容量。

加載因子過高,提高了空間利用率,但是同時增加了查詢時間成本。

加載因子過低,如果為0.5,那么減少了查詢時間成本,但是空間利用率低,另外還提高了rehash的次數。

所以,0.75是空間利用率和時間成本的折中。現在我們解釋一下為什么泊松分布在0.75的時候碰撞最小?理想的情況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵從泊松分布。當桶中元素大於8的時候,概率為百萬分之6,也就是說0.75作為加載因子,每個碰撞位置的鏈表長度超過8幾乎是不可能的。

5、HashMap紅黑樹的閾值為什么是8 ?

因為hash碰撞發送8次的概率非常小。但是如果真的發生了8次,說明hash碰撞發生的可能性已經很大了。后序會繼續發生碰撞,為防止碰撞,將鏈表轉化為紅黑樹。

6、為什么使用‘ 紅黑樹 ’?

由於二叉查找樹最壞的情況下會出現線性結構,退化為一個鏈表,查詢時間長。所以采用平衡二叉查找樹。其在插入和刪除的時候,會將高度保持在logN。看看為啥不用AVL樹,AVL樹實現較復雜,而且插入刪除性能差。因此實際環境中我們使用紅黑樹。

RB—Tree的特點

每個節點都是紅色或者黑色;

沒有相鄰的紅色節點;

根節點始終是黑色;

對每個節點,從該節點到其子孫節點的所有路徑上包含相同的黑色節點。

查詢,刪除,插入都是o(logN)的復雜度。

7、講一講synchronized ,可重入鎖以及底層原理?

synchronized是一個java關鍵字。synchronized可以修飾代碼塊,普通方法和靜態方法。修飾普通方法作用的是調用該方法的對象。修飾靜態方法修飾的是整個類對象。修飾代碼塊,作用的是調用 代碼塊的對象。

synchronized的作用是保證同一時刻最多只有一個線程被執行,其他線程只能等待當前線程執行完了以后才能執行該方法、代碼塊。從而保證線程安全,解決多線程中的並發同步問題。

synchrinized的特點

synchronized原理:依賴 JVM 實現。同步底層通過一個監視器對象(monitor)完成。同步代碼塊使用的是monitorenter和monitorexit指令,其中monitorenter指向同步代碼塊的開始位置,monitorexit指向結束位置。當執行monitorenter指令時,線程視圖去獲取鎖,也就是獲取monitor的執行權。當計數器為0時則可以成功獲取,獲取后將計數器加1。相應在執行monitorexit指令后,將鎖計數器設為0, 表明鎖被釋放。如果獲取鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放。

可重入鎖ReentrantLock及底層原理

可重入鎖的概念是表示自己可以再次獲取自己的鎖;依賴於API,顯示地加鎖Lock(),顯示地釋放鎖unLock()。

ReentrentLock增加了synchronized沒有的幾個高級功能。

1、等待可中斷。就是說正在等待的線性可以放棄等待,轉而去處理其他的任務。

2、可實現公平鎖。公平鎖指的是先等待的線程可以先獲得鎖。ReenTrantLock通過構造方法ReenTrantLock(boolean fail)指定公平還是非公平。

3、可實現選擇性的通知。利用Condition接口。ReenTrantLock的選擇性通知是由Condition接口來提供的。調用signAll()方法,只會喚醒注冊在該Condition實例上的所有等待線程。而synchronized 調用notifyAll()會喚醒所有的等待線程導致效率低下。

可重入鎖底層原理

使用AQS框架(構建鎖和同步器的框架)可以實現可重入鎖。首先狀態state初始化為0 ,表示未鎖定狀態。A線程Lock()時,調用tryAcquire()方法區獨占鎖,並將state加1 。此后,其他線程想要tryAcquire()時就會失敗,直到A線程調用unlock()釋放鎖,state為0 為止,其他線程才有機會獲取該鎖。當然釋放鎖之前,A線程是可以重復獲取該鎖的(state也會累加)。但是要記得釋放鎖,讓state回到0態。

阻塞隊列的原理

什么是阻塞隊列?阻塞隊列的實現原理是什么?如何使用?

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。

這兩個附加的操作是:在隊列為空時,獲取元素的線程會等待隊列變為非空。當隊列滿時,存儲元素的線程會等待隊列可用。
阻塞隊列常用於生產者和消費者的場景,生產者是往隊列里添加元素的線程,消費者是從隊列里拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者
也只從容器里拿元素。。

JDK7 提供了 7 個阻塞隊列。分別是:

ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。

LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。

PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。

DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。

SynchronousQueue:一個不存儲元素的阻塞隊列。

LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。

LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

Java 5 之前實現同步存取時,可以使用普通的一個集合,然后在使用線程的協作和線程同步可以實現生產者,消費者模式,主要的技術就是用好wait ,notify,notifyAll,sychronized 這些關鍵字。而在 java 5 之后,可以使用阻塞隊列來實現,此方式大大簡少了代碼量,使得多線程編程更加容易,安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口,它的主要用途並不是作為容器,而是作為線程同步的的工具,因此他具有一個很明顯的特性,當生產者線程試圖向
BlockingQueue 放入元素時,如果隊列已滿,則線程被阻塞,當消費者線程試圖從中取出一個元素時,如果隊列為空,則該線程會被阻塞,正是因為它所具有這個特性,所以在程序中多個線程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制線程之間的通信。

阻塞隊列使用最經典的場景就是 socket 客戶端數據的讀取和解析,讀取數據的線程不斷將數據放入隊列,然后解析線程不斷從隊列取數據解析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM