面经收集总结(来自网络)

面经

分布式及微服务

了解过分布式吗?

分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。

好处:系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。

系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。

服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。

补充:分布式与集群的区别?

分布式 主要的提法是 distributed

集群主要的提法是cluster

分布式是指通过网络连接的多个组件,通过交换信息协作而形成的系统。而集群,是指同一种组件的多个实例,形成的逻辑上的整体。

分布式一般会相互进行信息交流以及进行通信,集群一般不存在实质的信息交流,但是集群比如Zookeeper的节点都是对等的,但它自己就构成一个分布式系统。

CAP理论是什么?

一致性(consistency),可用性(availibity),分区容错性(partition tolerance)

任何分布式系统都无法同时满足三项,最多只能同时满足启动两项

接口的幂等性

幂等的意思是重复操作,接口的幂等性也就是接口被重复调用了,在前端不进行限制的情况下,同一个接口可能重复调用多次,为了避免类似重复下单的问题,可以通过以下几种方式来解决幂等性问题:

全局唯一ID,根据业务操作和内容生成全局唯一的ID,然后在执行操作前先判断是否已经存在该ID,如果不存在则将该ID进行持久化(存在数据库或者redis中),如果已经存在则证明该接口已经被调用过了.比如下单时可以生产一个流水号来作为该订单的唯一标识.

可以使用select+insert来进行判断,因为一般订单的ID都是唯一索引,在高并发场景下不推荐.

可以使用乐观锁解决,在表中可以添加一个version字段.

token机制,将token放在redis中.

消息中间件如何解决消息丢失问题

rabbitMQ,提供了事务机制,你可以在消息发送前提交事务,如果发送失败会自动回滚.

1
2
3
4
5
6
7
8
9
10
11
12
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback

// 这里再次重发这条消息
}

// 提交事务
channel.txCommit

但这样会比较耗性能,所以更推荐的做法是使用rabbitmq提供的confirm机制.confirm机制是异步的,在消费者收到生产者发送的消息后会回调ack,如果消费者接收失败会回调nack接口.

要想真正做到万无一失,我们不仅需要对Message进行持久化,还需要对Exchange,Queue也进行持久化,而rabbitMQ提供了这些持久化机制,因此使用一款好的消息中间件就可以很好的解决消息丢失问题.

什么是分布式事务?分布式事务的类型有哪些?

涉及到多个数据库操作的事务即为分布式事务,目的是为保证分布式系统中的数据一致性.

分布式事务类型:

二阶段提交2PC:第一步请求阶段通过协调者来统计表决结果,第二步执行表决后的结果,如果表决的结果是提交,那就提交执行,否则不执行提交.缺点是同步阻塞,而且万一协调者挂了就无法保证ACID.

三阶段提交3PC:在2PC的第一步拆分成了2步并且引入了超时机制,解决了2PC的痛点.第一步先向参与者发出一个信号,看看大家是否都能提交,如果可以就返回yes,否则返回no.第二步PreCommit阶段,预提交一下,如果参与者可以完成commit,就返回ack进确认,如果不能则放弃提交本次事务.第三步doCommit阶段,进行真正的事务提交.

分布式事务的解决方案有哪些?

XA:XA是基于二阶段事务实现的一种标准协议,目前主流数据库都已支持该协议.由于是二阶段提交,缺点很明显,不适合高并发场景.

补偿机制TCC:try,commit,cancel的缩写,try阶段进行检测,commit提交执行,只要try阶段成功了commit就一定会被执行,cancel业务出现错误时执行,回滚事务,释放资源.

消息中间件MQ:可以通过阿里的消息中间件RocketMQ来进行解决.RocketMQ支持带事务的消息,具体思路大概是这样的:

第一步:消息生产者向消息集群发送prepared消息,获取消息地址.

第二步:本地提交事务,并向集群发送确认消息.

第三步:通过第一步获取到的地址,访问消息并改变消息状态.

为什么要使用分布式锁

由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁的三种实现方式

  • 基于数据库实现分布式锁;
  • 基于缓存(Redis等)实现分布式锁;
  • 基于Zookeeper实现分布式锁;

基于数据库实现排他锁

  • 方案一
image-20210511095742033

获取锁

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');

对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。

  • 方案二
1
2
3
4
5
6
7
8
9
10
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`version` int NOT NULL COMMENT '版本号',
`PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

获取信息

1
select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

占有锁

1
update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

缺点:

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  3. 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:

​ 1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。

​ 2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

​ 3、非阻塞的?搞一个while循环,直到insert成功再返回成功。

​ 4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

基于Redis实现

获取锁

1
SET resource_name my_random_value NX PX 30000

my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。 NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。 PX 30000表示这个锁有一个30秒的自动过期时间。

setnx无法设置超时时间,如果进程解锁失败,那么这个锁就无法被解除,导致其他客户端都无法获得锁

使用RedLock红锁 ,Redis 官方提出的一种分布式锁的算法。

红锁流程:

  • 顺序向所有个节点请求加锁
  • 根据一定的超时时间来推断是不是跳过该节点
  • N/2+1个节点加锁成功并且花费时间小于锁的有效期
  • 认定加锁成功

红锁算法认为,只要 N/2+1 个节点加锁成功,那么就认为获取了锁, 解锁时将所有实例解锁。

Zookeeper和Redis分布式锁对比

image-20210511102345635

RPC框架有哪些?

  • RMI(远程方法调用)

JAVA自带的远程方法调用工具,不过有一定的局限性。

  • Hessian(基于HTTP的远程方法调用)

基于HTTP协议传输,在性能方面还不够完美,负载均衡和失效转移依赖于应用的负载均衡器,Hessian的使用则与RMI类似,区别在于淡化了Registry的角色,通过显示的地址调用,利用HessianProxyFactory根据配置的地址create一个代理对象,另外还要引入Hessian的Jar包。

  • Dubbo

基于Netty的高性能RPC框架,是阿里巴巴开源的

  • Thrift

Facebook 开源的一个 RPC 框架。支持非常多语言,包括在 WEB 开发中很常用的 PHP,以及最重要的 C++/Python/Java 等 WEB后端常用语言,当然,还包括很 cool 的 Ruby、Erlang。

Redis

Redis事务

MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础。Redis事务允许在一次单独的步骤中执行一组命令,Redis事务能够保证原子性。Redis还能够以乐观锁的形式提供更多的保证。

Redis为什么不支持事务回滚?

只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。 Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。

通过CAS实现乐观锁

Redis使用WATCH命令实现事务的“检查再设置”(CAS)行为。

作为WATCH命令的参数的会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个Null值,提醒用户事务运行失败。

缓存穿透、雪崩是什么

缓存穿透:某些key不在缓存中,一些恶意的请求故意查询这些key,由于这些key在缓存中不存在而造成大量请求涌入数据库查询造成压力。

解决方案:

  • 对查询结果为空的情况也进行缓存
  • 布隆过滤器:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

缓存雪崩:如果所有缓存数据设置的过期时间是相同的,那么所有缓存在同一时间内由于到期而失效,此时请求会全部进入数据库,这就是缓存雪崩.

解决方案:

  • 可以对缓存的过期时间设置一个随机值,避免缓存在同一时间过期.
  • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  • 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
  • 搭建Redis集群

RabbitMQ

如何保证消息不被重复消费

rabbitmq并没有提供防止消息重复消费的功能,只能在业务端去实现,如果业务是做了集群的,我们可以用redis帮助解决,具体做法是将消息的msgid和消费状态用redis存储起来,每次消费前先查看一下本次消费的消息状态,如果已经被消费过了,就可以不用消费.如果尚未消费,就可以进行消费,消费完把对应的状态改为已消费即可.

RabbitMQ有几种工作模式?

  • 简单队列
image-20210511105334200

一个生产者对应一个消费者。

  • work模式
image-20210511105359302

一个生产者对应多个消费者,但是只有一个消费者能够得到消息。轮询分发消息,依次发送给所有消费者。一个消息只能被一个消费者获取。

  • 发布/订阅模式
image-20210511105455493

一个消费者将消息首先发送到交换器,交换器绑定到多个队列,然后被监听该队列的消费者所接收并消费。

  • 路由模式
image-20210511105525139

生产者将消息发送到direct交换器,在绑定队列和交换器的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应key相同的队列,接着监听该队列的消费者消费消息。

  • 主题模式
image-20210511105617596

上面的路由模式是根据路由key进行完整的匹配(完全相等才发送消息),这里的通配符模式通俗的来讲就是模糊匹配

 符号“#”表示匹配一个或多个词,符号“*”表示匹配一个词。

常见MQ及其对比

image-20210511110819504

计算机网络

TCP如何保证可靠性,并简述一下TCP建立连接和断开连接的过程?

TCP保证可靠性:(1)序列号、确认应答、超时重传;数据到达接收方,接收方需要发出一个确认应答,表示收到该数据段,并且确认序列号会说明下次接收的数据序列号。如果发送方迟迟未收到确认应答,那么可能是发送数据丢失,也可能是确认应答丢失,这时发送方会等待一定时间后重传。(2)窗口控制与高速重发控制/快速重传(重复确认应答);TCP利用窗口控制来提高传输速度,意思是在一个窗口大小内,不一定等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。(3)拥塞控制;如果窗口定义的很大,发送端连续发送大量的数据,可能会造成网络的拥堵,甚至网络瘫痪。所以TCP为了防止这种情况而进行了拥塞控制。 TCP建立连接和断开连接过程:三次握手、四次挥手。

请回答一下HTTP和HTTPS的区别,以及HTTPS的优缺点。

区别:

(1)HTTP协议是以明文的方式在网络中传输数据,HTTPS协议传

输是数据时经过TLS加密后的,HTTPS具有更高的安全性。

(2)HTTPS在TCP三次握手之后还要进行SSL的握手。

(3)HTTPS协议需要服务器申请证书,客户端安装对应的根证书。

(4)HTTP协议端口号是80,HTTPS端口号是443。 HTTPS优点:

(1)传输过程中使用密钥加密,安全性更高。

(2)HTTPS可以认证用户和服务器,确保数据发送到正确的用户和服务器。 HTTPS缺点:

(1)HTTPS握手阶段延时较高(因为三次握手之后还有SSL握手)。

(2)HTTPS部署成本高:一方面HTTPS协议需要使用证书来验证自身安全性,所以需要购买CA证书;另一方面再用HTTPS协议需要进行加密解密的计算,占用CPU资源较多,需要的服务器配置或者数目增加。

IP地址的作用,以及MAC地址的作用。

MAC地址是硬件地址,用来定义网络设备的位置,主要由数据链路层负责;IP地址是IP协议提供的一种统一的地址格式,为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

TCP拥塞控制?以及达到什么情况的时候开始减慢增长速度。

拥塞控制是防止过多的数据注入网络,使得网络中的路由器或者链路过载。流量控制是点对点的通信量控制,而拥塞控制是全局的网络流量整体性的控制。发送双方都有一个拥塞窗口(cwnd)。 (1)慢开始:最开始发送方的拥塞窗口为1,由小到大递增。每经过一个传输轮次,拥塞窗口cwnd加倍(乘2)。当cwnd超过慢开始门限,则使用拥塞避免算法,避免cwnd增长过长。 (2)拥塞避免(算法):当cwnd超过慢开始门限,每经过一个往返时间RTT,cwnd就增长1。在慢开始和拥塞避免过程中,一旦发现网络拥塞,就把慢开始门限设置为当前值的一半,并且重新设置cwnd为1,重新慢启动。 (3)快重传:接收方每收到一个失序的报文段后就立即发出重复确认,发送方只要收到3个重复确认就立即重传。 (4)快恢复:当发送方连续收到三个重复确认,就将慢开始门限减半,将当前的窗口设置为慢开始门限,并采用拥塞避免算法。(采用快恢复算法时,慢开始只在建立连接和网络超时时才使用)

TCP/IP数据链路层的交互过程。

网络层等到数据链路层用mac地址作为通信目标,数据包到达网络准备往数据链路层发送的时候,首先会去自己的ARP缓存表(存ip-mac对应关系)去查找该目标IP的mac地址,如果查到了,就将目标ip的mac地址封装到链路层数据包的包头。如果缓存中没有找到,会发起一个广播(who is ip XXX tell ip XXX),所有收到广播的机器看这个ip是不是自己,如果是自己,就以单播的形式将自己的mac地址回复给请求的机器。

TCP和UDP的区别和各自适用的场景。

(1)连接:TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接;UDP是面向无连接的。 (2)服务对象:TCP是点对点的两点间服务,即一条TCP连接只能有两个端点;UDP支持一对一,一对多,多对一,多对多的交互通信。 (3)可靠性:TCP是可靠交付:无差错,不丢失,不重复,按序到达;UDP是尽最大努力交付,不保证可靠交付。 (4)拥塞控制,流量控制:TCP拥有拥塞控制和流量控制保证数据传输的安全性;UDP没有拥塞控制,网络拥塞不影响源主机的发送效率。 (5)报文长度:TCP是动态报文长度,即TCP报文长度根据接收方窗口大小和当前网络拥塞情况决定;UDP面向报文,不合并,不拆分,保留上面传下来的报文边界。 (6)首部开销:TCP首部开销大,首部20个字节;UDP首部开销小,8字节(源端口,目的端口,数据长度,校验和) 适用场景: 若通信数据完整性大于实时性,则应选择用TCP协议(文件传输、重要状态等);反之,则使用UDP协议(如视频传输、实时通信等)。

TCP四次挥手的时候,先发起方为什么会有一个TIME_WAIT状态,它的作用是什么?

1)保证最后一次握手报文能到接收方,如果接收方未收到会再次发送FIN+ACK,发起方可以进行超时重传。 (2)TIME_WAIT时间一般是2MSL。2MSL后这次连接的所有报文都会消失,不会影响下一次连接。

GET和POST的区别

1)概括:对于GET方式请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST方式请求,浏览器会先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 (2)区别: 1)get参数通过url传递,post放在request body中。 2)get请求在URL中传递参数的长度有限制(根据浏览器不同长度限制也不同),而post没有 3)get比post更不安全,因为参数暴露在URL中 4)get请求只能进行url编码,post支持多种编码方式 5)get 请求会被浏览器主动cache (缓存),post 则不会,除非手动设置 6)get请求会完整保留在浏览历史记录里,而post中的参数不会被保留 7)get和post本质上是TCP连接,并无差别,但是由于http的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同 8)GET产生一个TCP数据包,post产生两个数据包。

简单说一下http协议

1)简介 HTTP是一个基于TCP/IP通信协议来传递数据的协议。HTTP是一个属于应用层的面向对象的协议。HTTP协议工作于客户端-服务端架构之上,浏览器作为HTTP客户端通过URL向Web服务器发送所有请求,Web服务器根据接收到到的请求向客户端发送相应信息。 2)特点:

①简单快速:客户端向服务器发送请求时,只需传送请求方法和路径即可。②灵活:HTTP允许传输任意类型的数据对象。③无连接:限制每次连接只处理一个请求。服务器处理完客户请求,并收到客户应答后,即断开连接。④无状态:协议对于事务处理没有记忆能力。⑤支持B/S及C/S模式。⑥默认端口80。⑦基于TCP协议。 3)HTTP过程概述: HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP协议采用请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应内容包括协议版本、成功或者错误的代码、服务器信息、响应头部和响应数据。

TCP报文段及三次握手、四次挥手

TCP报文段

image-20210511114859511

1、端口号:用来标识同一台计算机的不同的应用进程。

1)源端口:源端口和IP地址的作用是标识报文的返回地址。

2)目的端口:端口指明接收方计算机上的应用程序接口。

TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。

2、序号和确认号:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。

3、数据偏移/首部长度:4bits。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8 = 60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。

4、保留:为将来定义新的用途保留,现在一般置0。

5、控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。

1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。

2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。

3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。

4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。

5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。

6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。

6、窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,因而窗口大小最大为65535。

7、校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。

8、紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。

9、选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。

10、数据部分: TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。

三次握手

image-20210511130236336

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

为什么需要三次?

第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

刚开始客户端处于closed 的状态,服务端处于listen 状态。然后 1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Sent 状态。

2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于SYN_REVD 的状态。

3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于establised 状态。

4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。

什么是半连接状态?

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, …

三次握手过程中可以携带数据吗?

很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。 而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。

四次挥手

四次挥手也一样,千万不要对方一个 FIN 报文,我方一个 ACK 报文,再我方一个 FIN 报文,我方一个 ACK 报文。然后结束,最好是说的详细一点,例如想下面这样就差不多了,要把每个阶段的状态记好。

刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。

2、第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。

3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态

5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

image-20210511131219105

每个状态的解释:

1
2
3
4
5
6
7
8
9
10
11
LISTEN - 侦听来自远方TCP端口的连接请求; 
SYN-SENT -在发送连接请求后等待匹配的连接请求;
SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;
ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
FIN-WAIT-2 - 从远程TCP等待连接中断请求;
CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
CLOSING -等待远程TCP对连接中断的确认;
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;
CLOSED - 没有任何连接状态;

操作系统

进程和线程以及它们的区别

  • 进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发

  • 线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的 实时性,实现进程内部的并发

  • 一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;

  • 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。

进程间的通信的几种方式

  • 管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

  • 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;

  • 消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;

  • 共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等;

  • 信号量:主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段;

  • 套接字:这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

进程同步的方式

  • 互斥量 Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问

  • 信号量 Semphare:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量

  • 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

    什么是死锁?死锁产生的条件?

    1). 死锁的概念

      在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。

    2). 死锁产生的四个必要条件

    互斥:至少有一个资源必须属于非共享模式,即一次只能被一个进程使用;若其他申请使用该资源,那么申请进程必须等到该资源被释放为止;

    占有等待:一个进程必须占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有;

    非抢占:进程不能被抢占,即资源只能被进程在完成任务后自愿释放

    循环等待:若干进程之间形成一种头尾相接的环形等待资源关系

死锁的处理基本策略和常用方法

解决死锁的基本方法主要有 预防死锁、避免死锁、检测死锁、解除死锁 、鸵鸟策略 等。

  (1). 死锁预防      死锁预防的基本思想是 只要确保死锁发生的四个必要条件中至少有一个不成立,就能预防死锁的发生,具体方法包括:

打破互斥条件:允许进程同时访问某些资源。但是,有些资源是不能被多个进程所共享的,这是由资源本身属性所决定的,因此,这种办法通常并无实用价值

打破占有并等待条件:可以实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,此进程暂不运行;只有当系统能满足当前进程所需的全部资源时,才一次性将所申请资源全部分配给该线程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们,但是在当前进程申请更多资源之前,它必须全部释放当前所占有的资源)。但是这种策略也存在一些缺点:在很多情况下,无法预知一个进程执行前所需的全部资源,因为进程是动态执行的,不可预知的;同时,会降低资源利用率,导致降低了进程的并发性

打破非抢占条件:允许进程强行从占有者哪里夺取某些资源。也就是说,但一个进程占有了一部分资源,在其申请新的资源且得不到满足时,它必须释放所有占有的资源以便让其它线程使用。这种预防死锁的方式实现起来困难,会降低系统性能

打破循环等待条件:实行资源有序分配策略。对所有资源排序编号,所有进程对资源的请求必须严格按资源序号递增的顺序提出,即只有占用了小号资源才能申请大号资源,这样就不回产生环路,预防死锁的发生。

  (2). 死锁避免的基本思想      死锁避免的基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。资源分配图算法和银行家算法是两种经典的死锁避免的算法,其可以确保系统始终处于安全状态。其中,资源分配图算法应用场景为每种资源类型只有一个实例(申请边,分配边,需求边,不形成环才允许分配),而银行家算法应用于每种资源类型可以有多个实例的场景。

  (3). 死锁解除

  死锁解除的常用两种方法为进程终止和资源抢占。所谓进程终止是指简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占是指从一个或多个死锁进程那里抢占一个或多个资源,此时必须考虑三个问题:

 (I). 选择一个牺牲品    (II). 回滚:回滚到安全状态    (III). 饥饿(在代价因素中加上回滚次数,回滚的越多则越不可能继续被作为牺牲品,避免一个进程总是被回滚)

进程有哪几种状态?

  • 就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源;
  • 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数;
  • 阻塞状态: 进程等待某种条件,在条件满足之前无法执行;
image-20210511135156856

线程有几种状态?

在 Java虚拟机 中,线程从最初的创建到最终的消亡,要经历若干个状态:

  • 创建(new)
  • 就绪(runnable/start)
  • 运行(running)
  • 阻塞(blocked)
  • 等待(waiting)
  • 时间等待(time waiting)
  • 消亡(dead/terminated)。
image-20210511135306777
image-20210511135354791

分页和分段有什么区别(内存管理)?

段式存储管理是一种符合用户视角的内存分配管理方案。在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)

  页式存储管理方案是一种用户视角内存与物理内存相分离的内存分配管理方案。在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。

两者的不同点:

目的不同:分页是由于系统管理的需要而不是用户的需要,它是信息的物理单位;分段的目的是为了能更好地满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息;

大小不同:页的大小固定且由系统决定,而段的长度却不固定,由其所完成的功能决定;

地址空间不同: 段向用户提供二维地址空间;页向用户提供的是一维地址空间;

信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制;

内存碎片:页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。

操作系统中进程调度策略有哪几种?

  • FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU

  • SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度

  • 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化

  • 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。

  • 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。

  • 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。

什么是虚拟内存?

虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是:每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上 注意,请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

虚拟内存的应用与优点

虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。虚拟内存的使用可以带来以下好处:

  • 在内存中可以保留多个进程,系统并发度提高
  • 解除了用户与内存之间的紧密约束,进程可以比内存的全部空间还大

页面置换算法

FIFO先进先出算法:在操作系统中经常被用到,比如作业调度(主要实现简单,很容易想到);

LRU(Least recently use)最近最少使用算法:根据使用时间到现在的长短来判断;

LFU(Least frequently use)最少使用次数算法:根据使用次数来判断;

OPT(Optimal replacement)最优置换算法:理论的最优,理论;就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。 时钟(CLOCK)置换算法:简单的CLOCK算法是给每一帧关联一个附加位,称为使用位。当某一页首次装入主存时,该帧的使用位设置为1;当该页随后再被访问到时,它的使用位也被置为1。对于页替换算法,用于替换的候选帧集合看做一个循环缓冲区,并且有一个指针与之相关联。当某一页被替换时,该指针被设置成指向缓冲区中的下一帧。当需要替换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一帧。每当遇到一个使用位为1的帧时,操作系统就将该位重新置为0;如果在这个过程开始时,缓冲区中所有帧的使用位均为0,则选择遇到的第一个帧替换;如果所有帧的使用位均为1,则指针在缓冲区中完整地循环一周,把所有使用位都置为0,并且停留在最初的位置上,替换该帧中的页。由于该算法循环地检查各页面的情况,故称为CLOCK算法,又称为最近未用(Not Recently Used, NRU)算法。

JDK

虚拟机是如何实现多态的

动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法.

垃圾回收GC

垃圾回收有哪些算法?

  1. 标记-清除
  2. 标记-复制
  3. 标记-整理
  4. 分代回收

具体详解:https://blog.csdn.net/dd864140130/article/details/50084471

如何判断一个对象是否应该被回收

这就是所谓的对象存活性判断,常用的方法有两种:

1.引用计数法;

2:对象可达性分析.由于引用计数法存在互相引用导致无法进行GC的问题,所以目前JVM虚拟机多使用对象可达性分析算法.

调用System.gc()会发生什么?

会通知JVM进行垃圾回收,但是什么时候回收由JVM自行决定。

底层是通过javaRuntime调用native的gc方法。

通过参数java -XX:+PrintGCDetails(新版本-Xlog:gc*)查看堆区和gc信息。

image-20210511164659579
image-20210511164718896

JVM架构图

img

执行引擎

分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。

  • 解释器:

解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

  • 编译器

JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。

  1. 中间代码生成器 – 生成中间代码

  2. 代码优化器 – 负责优化上面生成的中间代码

  3. 目标代码生成器 – 负责生成机器代码或本机代码

  4. 探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。

JUC

说说进程,线程,协程之间的区别

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程.

进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高.

线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位.同一进程中的多个线程之间可以并发执行.

协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。

协程其实就是一种用户级线程,是用户态调度任务,底层还是线程。

什么是多线程上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

Thread类中的start()和run()方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程创建,start()方法才会启动新线程。

怎么检测一个线程是否持有对象监视器

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。

Runnable和Callable的区别

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以方便获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务

wait()与sleep()的区别

  • sleep()来自Thread类,和wait()来自Object类.调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
  • sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU
  • sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()或者notifyAll()使用

为什么wait,nofity和nofityAll这些方法不放在Thread类当中

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上: (1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 (2)ReentrantLock可以获取各种锁的信息 (3)ReentrantLock可以灵活地实现多路通知 另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word.

什么是线程局部变量ThreadLocal

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

java中用到的线程调度算法是什么

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

Volatile特点

(1)保证可见性

(2)不保证原子性

(3)禁止指令重排

Volatile工作原理

JMM采取保守策略对volatile读写插入内存屏障:

1)每个 volatile 写前插入 StoreStore屏障

2)每个 volatile 写后插入 StoreLoad屏障

3)每个 volatile 读后插入 LoadLoad屏障

4)每个 volatile 读后插入 LoadStore屏障

由于volatile仅保证对单个volatile变量的读写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。功能上,锁更强大;在可伸缩性和执行性能上,volatile更有优势。

image-20210511171453382

在线程读写的时候加上内存屏障。

Java集合

集合框架图

img

poll()方法和remove()方法区别?

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

WeakHashMap与HashMap的区别是什么?

WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。

Comparator和Comparable的区别?

Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。

线程池

假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。

线程池关注的正是如何缩短或调整T1,T3时间

一个线程池包含以下四个基本组成部分

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

常见线程池

newSingleThreadExecutor 单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务 ②newFixedThreadExecutor(n) 固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行 ③newCacheThreadExecutor(推荐使用) 可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。 ④newScheduleThreadExecutor 大小无限制的线程池,支持定时和周期性的执行线程

java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。

为什么不建议使用 Executors静态工厂构建线程池

img

ThreadPoolExecutor 有哪些常用的方法?

  • submit()/execute():执行线程池

  • shutdown()/shutdownNow():终止线程池

  • isShutdown():判断线程是否终止

  • getActiveCount():正在运行的线程数

  • getCorePoolSize():获取核心线程数

  • getMaximumPoolSize():获取最大线程数

  • getQueue():获取线程池中的任务队列

  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程这些方法可以用来终止线程池、线程池监控等。

submit和 execute两个方法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回z值,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;

shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

了解过线程池的工作原理吗?

ThreadPoolExecutor类图

image-20210511173925307

线程池工作流程图

image-20210511173946003
image-20210511174039733

当线程池中有任务需要执行时,线程池会判断,如果线程数量没有超过核心数量就会新建线程池进行任务执行

如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

线程池中核心线程数量大小怎么设置?

「CPU密集型任务」:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

「IO密集型任务」:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为**2*CPU核心数**。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

线程池为什么需要使用(阻塞)队列

  • 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

  • 创建线程池的消耗较高。

线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

了解线程池状态吗?

image-20210511174608895

RUNNING:线程池的初始化状态,可以添加待执行的任务。

SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务

STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。

TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。TERMINATED:线程池终止状态。

知道线程池中线程复用原理吗

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

线程池创建需要的那几个核心参数的含义

ThreadPoolExecutor 最多包含以下七个参数:

  • corePoolSize:线程池中的核心线程数

  • maximumPoolSize:线程池中最大线程数

  • keepAliveTime:闲置超时时间

  • unit:keepAliveTime 超时时间的单位(时/分/秒等)

  • workQueue:线程池中的任务队列

  • threadFactory:为线程池提供创建新线程的线程工厂

  • rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

JMM

Java 内存模型定义了哪 8 个操作来完成主内存和工作内存的交互操作

image-20210512143611229

read:把一个变量的值从主内存传输到工作内存中

load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中

use:把工作内存中一个变量的值传递给执行引擎

assign:把一个从执行引擎接收到的值赋给工作内存的变量

store:把工作内存的一个变量的值传送到主内存中

write:在 store 之后执行,把 store 得到的值放入主内存的变量中

lock:作用于主内存的变量

unlock

内存模型三大特性

  1. 原子性

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。

  1. 可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。JMM 内部的实现通常是依赖于所谓的 内存屏障 ,通过 禁止某些重排序 的方式,提供内存 可见性保证 ,也就是实现了 各种 happen-before 规则 。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。

主要有有三种实现可见性的方式:

volatile,会 强制 将该变量自己和当时其他变量的状态都 刷出缓存

synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。

final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

volatile 并不能保证操作的原子性。

  1. 有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。

也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

先行发生原则(Happen-Before)是什么

JVM 规定了先行发生原则,让一个操作 无需控制 就能先于另一个操作完成,由于 指令重排序 的存在,两个操作之间有happen-before关系, 并不意味着前一个操作必须要在后一个操作之前执行。 仅仅要求前一个操作的执行结果对于后一个操作是可见的,并且前一个操作 按顺序 排在第二个操作之前。

volatile

volatile保证可见性、有序性、不保证原子性。

volatile可见性的实现:

volatile定义:

  • 当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
  • 写操作会导致其他线程中的缓存无效

这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。

volatile变量的禁止指令重排序的实现

volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。

硬件层面的“内存屏障”:

  • sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见
  • lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
  • mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能
  • lock 前缀:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。

JMM层面的“内存屏障”:

  • LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

HashMap源码

https://blog.csdn.net/qq_37692051/article/details/83050469

排序、查找算法

排序算法

image-20210512150426108
image-20210512150444478
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
* 冒泡排序
* @param a
*/
public static void insertSort(int[] a) {
for (int i = 1; i < a.length; i++) {
// 判断后面的数是否比前面的数大
if (a[i] < a[i - 1]) {
// 大的话就先存储,比前面数小就不操作
int temp = a[i];
// 从存储的数开始往前比较,小就交换
for (int j = i; j > 0; j--) {
//如果比前面的数小则交换
if (a[j] < a[j - 1]) {
a[j] = a[j - 1];
a[j - 1] = temp;
} else {
break;
}
}
// 打印每一轮过程
ArrGenerator.printArr(a);
}
}
}

/**
* 希尔排序
* @param a
*/
public static void shellSort(int[] a) {
// 设定gap步长
int j;
for (int gap = a.length / 2; gap > 0; gap /= 2) {
// 每次以gap/2为步长为一组
for (int i = gap; i < a.length; i++) {
int temp = a[i];
// 这一步最不好理解,j=i,i=gap,temp就是每个子数组的最后一个值,a[j-gap]是前一个值。进行插入排序
// 其实就是对以gap为步长的数进行插入排序(交换),j-=gap表示以gap步长数组的上一个值
for (j = i; j >= gap && temp < a[j - gap]; j -= gap) {
a[j] = a[j - gap];
}
// 当j小于gap的时候表示前面已经没有该子数组的值了,
a[j] = temp;
}

}
}


/**
* 快速排序
* @param a
* @param left
* @param right
*/
public static void quickSort(int[] a, int left, int right) {
if (left < right) {
int partition = partition(a, left, right);
quickSort(a, left, partition - 1);
quickSort(a, partition + 1, right);
}
}

public static int partition(int[] a, int left, int right) {
// 取第最左边为基准
int base = a[left];
while (left < right) {
// 从右指针开始判断 注意此处不能先判断左指针
while (left < right && a[right] >= base) {
right--;
}
// 将小值赋到前面
a[left] = a[right];
// 从左指针开始判断
while (left < right && a[left] <= base) {
left++;
}
// 将大值赋值到后边
a[right] = a[left];
}
// 结束后将基准赋值到中间(此时left与right相等)
a[left] = base;
return left;
}

/**
* 冒泡排序
* @param a
*/
public static void bubbleSort(int[] a) {
if (a.length < 1) {
return;
}
for (int i = 0; i < a.length-1; i++) {
int temp;
for (int j = 0; j < a.length-1; j++) {
if (a[j] > a[j+1]){
temp=a[j+1];
a[j+1] = a[j];
a[j] = temp;
}
}
}
}

public static void selectSort(int[] a) {
int temp;
for (int i = 0; i < a.length; i++) {
// 每轮循环第一个值设为默认最小值
int min=i;
// j=i+1,表示从a[i]后面一个元素开始直到结束,找到最小值
for (int j = i+1; j < a.length; j++) {
if (a[j] < a[min]){
min = j;
}
}
// 将最小值和第一个值交换
temp=a[i];
a[i] = a[min];
a[min] = temp;
}
}

/**
* 快速排序第二种
* @param a
* @param low
* @param high
*/
public static void quickSort2(int[] a, int low, int high) {
// 已经排完
if (low <= high) {
return;
}
int left = low;
int right = high;
// 基准值
int pivot = a[left];
while (left < right) {
// 从后往前
while (left < right && a[right] >= pivot) {
right--;
}
// 从前往后
while (left < right && a[left] <= pivot) {
left++;
}
a[right] = a[left];
}
// 放置基准值
a[left] = pivot;
// 分治
quickSort2(a,low,left - 1);
quickSort2(a,left + 1,high);
}

查找算法

查找算法分为

  • 顺序查找
  • 二分查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 不使用递归的二分查找
*title:commonBinarySearch
*@param arr
*@param key
*@return 关键字位置
*/
public static int commonBinarySearch(int[] arr,int key){
int low = 0;
int high = arr.length - 1;
int middle = 0; //定义middle
if(key < arr[low] || key > arr[high] || low > high){
return -1;
}

while(low <= high){
middle = (low + high) / 2;
if(arr[middle] > key){
//比关键字大则关键字在左区域
high = middle - 1;
}else if(arr[middle] < key){
//比关键字小则关键字在右区域
low = middle + 1;
}else{
return middle;
}
}

return -1; //最后仍然没有找到,则返回-1
}
  • 哈希查找
  • 分块查找
  • 斐波那契查找

其他

为什么在重写equals方法的时候要重写hashcode的方法?

判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。

如何实现深拷贝

  • 我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。
  • 重写clone()方法

Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。

  • 序列化拷贝

Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。

Https请求流程?

img

.客户端想服务器发起HTTPS的请求,连接到服务器的443端口;

2.服务器将非对称加密的公钥传递给客户端,以证书的形式回传到客户端

3.服务器接受到该公钥进行验证,就是验证2中证书,如果有问题,则HTTPS请求无法继续;如果没有问题,则上述公钥是合格的。(第一次HTTP请求)客户端这个时候随机生成一个私钥,成为client key,客户端私钥,用于对称加密数据的。使用前面的公钥对client key进行非对称加密

4.进行二次HTTP请求,将加密之后的client key传递给服务器;

5.服务器使用私钥进行解密,得到client key,使用client key对数据进行对称加密

6.将对称加密的数据传递给客户端,客户端使用对称解密,得到服务器发送的数据,完成第二次HTTP请求。

HTTPS=HTTP+加密+认证+完整性保护

一面

谈项目,业务流程

了解过分布式吗?

你用到了Redis,那么Redis还可以做什么?

了解分布式锁吗?

GC算法有哪几种?

数据库索引的实现方式?

B+树、红黑树有了解过吗?

写一道算法,合并两个有序链表

(其他记得不太清楚了,忘了记录)

二面

挑一个你熟悉的技术栈讲一讲。

你说Redis是单线程的,那么比如遇到1000个并发请求它是怎么处理的,为什么单线程去处理并发量效率会高?

了解过数据库索引吗?是什么数据结构?有什么特点?

主键索引和非主键索引有什么区别?

深度分页有了解过吗?怎么去优化?

HashMap里也有一种树是什么树?

对树(二叉查找树、二叉平衡树、B、B+树,红黑树)有什么了解?

写一个dp,青蛙跳台阶。

如果给你1T的文件,里面只保存的姓名,如何找到重复姓名频率最高的10个?

1T文件如何去拆分?(提示:Hashmap的hash算法)

为什么下水道井盖是圆的?

HR面经

你简历里写了两个项目,能介绍一下吗?

项目开发完成后有什么收获?

大学有参加竞赛或者实习吗?

哪里人?

有考虑过考研吗?

目前收到了哪些公司的offer?

如果你来到我们的团队,你的优势和劣势会是什么?

能实习多久?

然后说一周内会给我发正式的电子邮件

自我介绍

个人优点

比较正能量,学习能力比较强,责任心和团队意识比较强,吃苦耐劳,敢于挑战自我。

个人缺点

未来规划和理想

项目遇到的困难,怎么解决的?

你有什么兴趣爱好

简单介绍家庭

重点强调温馨和睦的家庭氛围、强调父母对自己教育的重视、家庭成员对自己工作的支持、自己对家庭的责任感等。

成功或者失败的事

成功(快乐、自豪)的事:冷静看待-智慧努力-成功组织-再接再厉 失败(痛苦、自责)的事:正确认知-敢于面对-分析原因-磨练改变。

你有什么要问我的吗

  • 这个职位为什么空缺?
  • 这个职位换过多少人?主要原因是什么呢?
  • 这个职位在贵公司的具体职责是什么?
  • 您希望作为一个实习生,需要到达什么目标比较满意呢?
  • 前这个职位最紧要任务是什么?如果我有幸入职,您希望我三个月完成哪些工作?
  • 贵公司希望通过这个职位实现的长期目标是什么?
  • 这一职位将面临的最大困难是什么?
  • 这个位的工作业绩如何评估?
  • 这一职位会得到哪些支持,例如从人力上和财务上?
  • 我在制定自己的工作目标、工作期限以及工作方式上有多大的自由度?
  • 在这个职位做出出色业绩的员工,将有什么发展机会?大约多长时间能实现?
  • 贵公司最近有什么重大规划?
  • 贵公司在产品和服务上的成功之处是什么?

入职后是否有相关的职位技能培训

大学里印象最深的一件事