腾讯一面面试复盘
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 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。