腾讯后端开发高频问题和答案
🌟表示出现的次数,一个🌟表示出现一次。
Java基础
1、ArrayList与LinkedList区别 (🌟🌟🌟)
回答:ArrayList底层使用的是 Object数组;LinkedList底层使用的是 双向链表 数据结构。
ArrayList:增删慢、查询快,线程不安全,对元素必须连续存储。
LinkedList:增删快,查询慢,线程不安全。
2、volatile与synchronized的区别?(🌟🌟)
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
-
volatile
关键字是线程同步的 轻量级实现,所以volatile
性能肯定比synchronized
关键字要好。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块。 -
volatile
关键字能保证 数据的可见性,但不能 保证数据的原子性。 synchronized 关键字 两者都能保证。 -
volatile
关键字主要用于 解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是 多个线程之间访问资源的同步性。
3、Java线程池核心参数与工作流程,拒绝策略(🌟🌟)
线程池有7大核心参数,分别是
corePoolSize:核心线程数
maximumPoolSize:线程池中最大线程数
keepAliveTime:多余空闲线程数的存活时间,当前线程数大于corePoolSize,并且等待时间大于keepAliveTime,多于线程或被销毁直到剩下corePoolSize为止。
TimeUnit unit:keepAliveTime的单位。
workQueue:阻塞队列,被提交但未必执行的任务。
threadFactory:用于创建线程池中工作线程的线程工厂,一般用默认的。
handler:拒绝策略,当堵塞队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)。
线程池中的执行流程:
(1)当线程数小于核心线程数的时候,使用核心线程数。
(2)如果核心线程数小于线程数,就将多余的线程放入任务队列(阻塞队列)中
(3)当任务队列(阻塞队列)满的时候,就启动最大线程数.
(4)当最大线程数也达到后,就将启动拒绝策略。
有四种拒绝策略
1.ThreadPoolExecutor.AbortPolicy
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常(即后面提交的请求不会放入队列也不会直接消费并抛出异常);
2.ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃(也不会抛出任何异常,任务直接就丢弃了)。
3.ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务(丢弃掉了队列最前的任务,并不抛出异常,直接丢弃了)。
4.ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务(不会丢弃任务,最后所有的任务都执行了,并不会抛出异常)
4、HashMap与ConcurrentHashMap的区别(🌟🌟)
回答:在jdk1.7之前HashMap是基于数组和链表实现的,而且采用头插法。
而jdk1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。采用尾插法。
HashMap是线程不安全的,其主要体现:
1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
ConcurrentHashMap是线程安全的
(1)在jdk1.7的时候是使用分段锁segment,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
(2)在jdk1.8的时候摒弃了 Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS 来操作。synchronized只锁定当前链表或红黑二叉树的首节点。
计算机网络
1、浏览器上输入地址后的整个请求过程(🌟🌟🌟🌟🌟)
1、解析URL得到发送给web的信息,并生产 HTTP 请求信息
3、通过 DNS 获取到 IP 后,就可以把 HTTP 的传输⼯作交给操作系统中的协议栈。
4、经过TCP三次握手建立连接进行数据传输。
5、TCP 模块在执行连接、收发、断开等各阶段操作时,都需要委托 IP 模块将数据封装成网络包发送给通信对象。
6、生成了 IP 头部之后,接下来网络包还需要在 IP 头部的前面加上 MAC 头部。
7、后续还会经过网卡、交换机和路由器到对端,然后就是一个反向的过程。
2、TCP三次握手四次挥手及对应的状态(🌟🌟🌟🌟)
三次握手
-
开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态 -
客户端会随机初始化序号( client_isn ),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。 -
服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号( server_isn ),将此序号填入TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1 , 接着把 SYN和 ACK 标志位置为 1 。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。 -
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。 -
服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
四次挥手
-
客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进 FIN_WAIT_1 状态。 -
服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。 -
客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。 -
等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。 -
客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入TIME_WAIT 状态 -
服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。 -
客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。
【注意是:主动关闭连接的,才有 TIME_WAIT 状态。】
3、TCP为什么要三次握手(🌟🌟🌟)
回答:“因为三次才能保证双方具有接收和发送的能力。”,这样回答或许有些片面,可以在回答以下几点。
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不᯿复、不丢弃和按序传输。
不使用两次和四次的原因:
两次:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
四次:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
MySQL
1、MySQL的默认隔离级别、不同等级隔离级别解决的问题与实现原理(🌟🌟🌟🌟🌟)
未提交读(Read Uncommitted):在事务 A 读取数据时,事务 B 读取和修改数据加了共享锁。这种隔离级别,会导致脏读、不可重复读以及幻读。
已提交读(Read Committed):在事务 A 读取数据时增加了共享锁,一旦读取,立即释放锁,事务 B 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务 A 在读取数据时,事务 B 只能读取数据,不能修改。当事务 A 读取到数据后,事务 B才能修改。这种隔离级别,可以避免脏读,但依然存在不可重复读以及幻读的问题。
可重复读(Repeatable Read):在事务 A 读取数据时增加了共享锁,事务结束,才释放锁,事务 B 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务A 在没有结束事务时,事务 B 只能读取数据,不能修改。当事务 A 结束事务,事务 B 才能修改。这种隔离级别,可以避免脏读、不可重复读,但依然存在幻读的问题。
可序列化(Serializable):在事务 A 读取数据时增加了共享锁,事务结束,才释放锁,事务 B 读取修改数据时增加了表级排他锁,直到事务结束才释放锁。可序列化解决了脏读、不可重复读、幻读等问题,但隔离级别越来越高的同时,并发性会越来越低。
2、索引的数据结构对比Hash、B树与B+树?(🌟🌟🌟)
回答:当存储同数量级的数据的时候,B+树的高度比B树的高度小,这样的话进程IO操作的次数就少,效果就高。因为B+树的所有非叶子节点只存索引,数据存在叶子节点,一般3层的树高度,即可存千万级别的数据,而B数不行。(具体的计算可以到网上去看看,有面试官可能会问你怎么算出来的。)
Hash与B+树的区别
-
B+ 树可以进行范围查询,Hash 索引不能。 -
B+ 树支持联合索引的最左侧原则,Hash 索引不支持。 -
B+ 树支持 order by 排序,Hash 索引不支持。 -
Hash 索引在等值查询上比 B+ 树效率更高。 -
B+ 树使用 like 进行模糊查询的时候,like 后面(比如%开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。
操作系统
1、进程的状态(🌟🌟🌟🌟)
进程的三种基本状态:
运行态:占有cpu,并且就在cpu上执行。
就绪态:已经具备运行条件,但由于没有空闲cpu,而暂时不能运行。(也就是cpu没有调度到它)
阻塞态:因等待某一事件而不能运行。
其实这个很好理解,如果看过我之前关于线程的讨论的话应该很容易理解。注意:单核处理机同一时间只有一个进程处于运行态,双核则是两个。
还有两个状态:
创建态:进程正在被创建,系统为其初始化PCB,分配资源。
终止态:进程正在从系统中撤销,回收进程的资源,撤销其PCB。
下面一张图可看出这五个状态的关系:
2、死锁的产生条件与解决方案(🌟🌟🌟)
(1)互斥使用(资源独占):一个资源每次只能给一个进程使用
(2)占有且等待(请求和保持,部分分配):进程在申请新的资源的同时保持对原有资源的占有
(3)不可抢占(不可剥夺):资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放
(4)循环等待:存在一个进程等待队列 {P1 , P2 , … , Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路。当死锁产生的时候一定会有这四个条件,有一个条件不成立都不会造成死锁。
产⽣死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
那么避免死锁问题就只需要破环其中⼀个条件就可以,最常⻅的并且可⾏的就是使⽤资源有序分配法,来破环环路等待条件。
Redis
1、Redis基本数据类型和简述Redis使用场景(🌟🌟🌟)
回答:常见的有五种基本数据类型和三种特殊数据类型,
基本数据结构:String、 list、set、zset和hash,三种特殊数据类型:位图(bitmaps) 、计数器(hyperloglogs)和地理空间(geospatial indexes)。
String:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
list:发布与订阅或者说消息队列、慢查询。
hash:系统中对象数据的存储。
set:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
zset:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
2、Redis如何实现分布式锁?(🌟🌟)
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!