面试哈啰,差点要了狗命~
这几天面试哈啰,本来以为小小哈啰可以轻松拿捏,但没成想,问的还挺深,差点要了狗命,一起来看看吧~
创建一个线程时底层发生了什么?
在 Java 中,创建一个线程时,底层发生了以下几个主要步骤:
- 分配线程栈:线程对象被创建后,Java 虚拟机会为该线程分配一个独立的线程栈(Thread Stack),用于存储该线程的方法调用、局部变量等信息。
- 初始化线程属性:设置线程的属性,例如优先级、守护线程标志等。
- 调用线程的 start() 方法:当调用线程对象的 start() 方法时,会触发 Java 虚拟机调用 run() 方法,并启动线程的执行。
- 启动线程执行:Java 虚拟机会在后台创建并启动一个操作系统线程来执行 Java 线程,每个 Java 线程对应一个底层的操作系统线程。
- 线程的调度和执行:操作系统负责线程的调度,按照线程优先级、时间片轮转等算法来确定线程的执行顺序,线程开始执行其业务方法。
- 线程执行结束:当线程的业务方法执行完毕或线程抛出异常导致执行终止时,线程进入终止状态。
说说线程的生命周期?
线程生命周期总共有以下 6 种:
- NEW(新建状态):new Thread() 时线程的状态。
- RUNNABLE(可运行/运行状态):调用 start() 方法后的状态。
- BLOCKED(阻塞状态):调用了 synchronized 加锁之后的状态。获得锁之后就从 BLOCKED 状态变成了 RUNNABLE 状态。
- WAITING(无时限等待状态):调用了 wait() 方法之后会进入此状态。
- TIMED_WAITING(有时限等待状态):调用了 sleep(long millis) 方法之后会进入此状态。
- TERMINATED(终止状态):线程任务执行完成之后就变成此状态。
线程状态的转换如下图所示:
使用线程池时核心线程数和最大线程数如何设计?
核心线程数设计分为以下两种情况:
- 计算型任务:根据实际业务场景设置,参考值为:CPU 核数+1。
- IO 型任务:根据实际业务场景设置,参考值为:2*CPU 核数+1。
PS:为什么要 CPU 核数+1,而不是 CPU 核数?多出来的一个线程可用于更加平滑的线程替补,例如有现成执行完,或休眠、中断等,多出来的一个线程就可以尽快的进行补充,避免了 CPU 空闲。
如果,任务量波动不大,可以将最大线程数设置和核心线程数一样的数量,避免临时线程创建和销毁的性能开销,如果波动比较大,可根据实际业务场景设置,参考值为 2 倍的核心线程数。
说说你都用过哪些微服务组件?
微服务常见组件如下:
- Nacos:注册中心和配置中心。
- 注册中心:负责维护微服务实例的注册信息,包括服务地址和服务状态。服务启动时向注册中心注册自身,服务消费者通过注册中心发现服务提供者的地址,实现服务间的动态发现与路由。
- 配置中心:集中管理微服务应用的配置信息,支持动态配置更新,使得微服务可以在运行时获取或更新配置,便于配置的统一管理和版本控制。
- Spring Cloud OpenFeign:RESTful(基于标准的 HTTP 协议和 URI(Uniform Resource Identifier)的一组约束和原则)通讯,负责微服务之间的调用。
- Spring Cloud LoadBalancer:负载均衡器(客户端)负责微服务调用时节点的选取。
- Spring Cloud Gateway:网关,微服务架构的入口,提供统一的接口访问入口,处理所有进来的请求,包括路由转发、协议转换、安全认证(如OAuth)、速率限制、负载均衡等,对外屏蔽微服务细节。
- Sentinel:限流、熔断降级,防止服务雪崩效应,增强系统的稳定性和韧性。
- Seata:分布式事务,保证微服务间一组执行方法的原子性(要么一起执行成功,要么一起执行失败)。
- Skywalking:分布式链路追踪,记录微服务调用日志,监控系统和方便排查问题。
讲一下Dubbo运行原理?
Dubbo 是一款高性能、轻量级的开源 RPC(远程过程调用)框架,主要用于构建分布式服务和微服务架构。
要说 Dubbo 运行流程就不得不先来了解一下 Dubbo 的核心组件了,因为 Dubbo 的交互流程是和核心组件息息相关的。
Dubbo 核心组件有以下几个:
- 服务提供者(Provider):暴露服务的应用,通过 Dubbo 框架将自身的服务接口及实现注册到注册中心。
- 服务消费者(Consumer):调用远程服务的应用,从注册中心订阅所需的服务,然后通过远程调用消费服务。
- 注册中心(Registry):集中管理服务的地址信息,服务提供者和服务消费者均在此注册或订阅服务信息。常见的注册中心有 ZooKeeper、Nacos 等。
Dubbo 运行流程如下图所示:
它的执行流程如下:
- 服务提供者会将实例(URL 地址)注册到注册中心,注册中心负责对数据进行聚合(健康检测)。
- 消费者从注册中心读取地址列表并订阅变更,每当地址列表发生变化,注册中心将最新的列表通知到所有订阅的消费者实例。
- 消费者得到服务实例之后,通过 Dubbo 内置的负载均衡策略,选择其中的一个节点,之后使用 RPC 的方式与服务提供者建立连接,并进行通讯和服务调用。
更详细的调用流程如下:
Dubbo通讯协议
Dubbo 框架提供了自定义的高性能 RPC 通信协议:基于 HTTP/2 的 Triple 协议和基于 TCP 的 Dubbo2 协议。除此之外,Dubbo 框架支持任意第三方通信协议,如官方支持的 gRPC、Thrift、REST、JsonRPC、Hessian2 等,更多协议可以通过自定义扩展实现。这对于微服务实践中经常要处理的多协议通信场景非常有用。
Dubbo 框架不绑定任何通信协议,在实现上 Dubbo 对多协议的支持也非常灵活,它可以让你在一个应用内发布多个使用不同协议的服务,并且支持用同一个 port 端口对外发布所有协议。
通过 Dubbo 框架的多协议支持,你可以做到:
- 将任意通信协议无缝地接入 Dubbo 服务治理体系。Dubbo 体系下的所有通信协议,都可以享受到 Dubbo 的编程模型、服务发现、流量管控等优势。比如 gRPC over Dubbo 的模式,服务治理、编程 API 都能够零成本接入 Dubbo 体系。
- 兼容不同技术栈,业务系统混合使用不同的服务框架、RPC 框架。比如有些服务使用 gRPC 或者 Spring Cloud 开发,有些服务使用 Dubbo 框架开发,通过 Dubbo 的多协议支持可以很好的实现互通。
- 让协议迁移变的更简单。通过多协议、注册中心的协调,可以快速满足公司内协议迁移的需求。比如如从自研协议升级到 Dubbo 协议,Dubbo 协议自身升级,从 Dubbo 协议迁移到 gRPC,从 HTTP 迁移到 Dubbo 协议等。
Dubbo负载均衡策略
目前 Dubbo(3.X)内置了如下负载均衡算法如下:
- Weighted Random LoadBalance(加权随机):默认负载均衡算法,默认权重相同。按权重设置随机概率。缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
- RoundRobin LoadBalance(加权轮询):借鉴于 Nginx 的平滑加权轮询算法,默认权重相同,按公约后的权重设置轮询比率,循环调用节点。缺点:同样存在慢的提供者累积请求的问题。
- LeastActive LoadBalance(最少活跃优先+加权随机):背后是能者多劳的思想,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。
- Shortest-Response LoadBalance(最短响应优先+加权随机):更加关注响应速度,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机。使得响应时间越快的提供者,处理更多的请求。缺点:可能会造成流量过于集中于高性能节点的问题。
- ConsistentHash LoadBalance(一致性哈希):确定的入参,确定的提供者,适用于有状态请求。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
- P2C LoadBalance(随机选择两个节点+连接数较小):随机选择两个节点后,继续选择“连接数”较小的那个节点。对于每次调用,从可用的 provider 列表中做两次随机选择,选出两个节点 providerA 和 providerB,比较 providerA 和 providerB 两个节点,选择其“当前正在处理的连接数”较小的那个节点。
- Adaptive LoadBalance(自适应负载均衡):在 P2C 算法基础上,选择二者中 load 最小的那个节点,是一种能根据后端实例负载自动调整流量分布的算法实现,它总是尝试将请求转发到负载最小的节点。
RPC和HTTP有什么区别?
RPC(Remote Procedure Call,远程过程调用)和 HTTP(Hypertext Transfer Protocol,超文本传输协议)都是用于服务间通讯的,它们主要区别如下:
- 概念和使用场景不同:
- RPC:RPC 是一种通信模式,允许一个程序在另一个地址空间上执行远程计算过程,使得客户端调用远程服务就像调用本地方法一样。
- HTTP:HTTP 是一个应用层协议,用于在客户端和服务器之间传输文本、图像、视频等超媒体资源,通常用于 Web 应用之间的通信。
- 传输数据不同:
- RPC:RPC 通常基于二进制数据传输,可以使用更高效的序列化方式(如 Protobuf、Thrift)进行数据交换。
- HTTP:HTTP 使用文本协议,请求和响应数据通常是基于文本格式(如 JSON、XML)进行传输。
- 传输效率与性能不同:
- RPC:因为 RPC 通常使用更高效的二进制序列化(如 Protobuf、Thrift),减少了数据传输的体积,且由于其针对性的设计,往往在性能上更为优越,特别是在大量小数据包的传输场景。
- HTTP:传统上使用文本格式如 JSON 进行数据交换,这可能导致更大的数据包和更多的序列化/反序列化开销,但在 HTTP/2 中引入了头部压缩和多路复用,提升了效率。
什么是序列化?
序列化是将对象转换为字节流的过程,可以用于数据持久化、数据传输等场景。序列化的主要目的是将对象在内存中的状态转换为可存储或传输的形式。
让你设计一个RPC框架,如何考虑数据序列化问题?
数据序列化需要考虑的以下问题:
- 性能问题:选择高性能的序列化库至关重要。二进制序列化(如 Protocol Buffers, Apache Thrift, FlatBuffers)通常比文本格式(如 JSON、XML)更高效,因为它们占用的空间小,序列化和反序列化的速度更快。对于高性能要求的场景,应优先考虑这些二进制格式。
- 安全性:在序列化过程中,应考虑数据的安全性,避免敏感信息的泄露。可以采用加密序列化内容、过滤敏感字段或使用安全的传输层协议(如 TLS/SSL)来增加安全性。
- 兼容性:良好的版本兼容性是长期维护 RPC 框架的关键。设计时要考虑向前和向后兼容,即新老版本的序列化库应能互相理解和处理对方生成的数据格式。可以采用预留字段、版本标识符等机制来支持这一点。
- 跨语言支持:RPC 框架往往需要支持多种编程语言,因此选择一种跨语言的序列化方案是必要的。Protocol Buffers、Apache Thrift、Avro 等都是很好的选择,它们提供了多种语言的编解码库。
- 可扩展性:设计时应考虑到未来可能增加的数据结构和字段,序列化方案应易于扩展,支持动态字段、自定义类型等特性。
- 可配置性:允许用户根据实际需求选择或切换序列化策略。例如,对于对性能要求极高的场景,用户可以选择最高效的序列化方式;而对于调试或日志记录,可能会偏好人类可读性更好的格式。
- 异常处理:在序列化或反序列化过程中可能会遇到错误(如数据损坏、不兼容的版本等)。框架应能优雅地处理这些异常,并提供清晰的错误信息,帮助开发者诊断问题。
说说索引的底层实现?
MySQL 默认的数据库引擎 InnoDB 主要使用的是 B+ 树实现的,它的特点是:
- 非叶子节点不存储数据:仅存储键值和指向子节点的指针。
- 叶子节点存储数据:所有实际的数据记录或者指向记录的指针都存放在叶子节点中,并且叶子节点通过指针相连,形成了一个有序链表,便于范围查询。
- 高度平衡:通过分裂和合并保持树的高度平衡,从而保证查询效率稳定。
- 高效率的磁盘I/O:由于树的高度较低,即使在磁盘 I/O 操作中也能保持较高的查询效率。
为什么用B+树?
索引使用 B+ 树的主要原因包括以下几点:
- 高效的查找和范围查询:
- B+ 树是一种多路平衡查找树,具有良好的有序性和平衡性,可以快速定位目标数据并支持高效的范围查询。
- B+ 树通过多级索引结构,能够在保持有序性的同时,减少树的深度,降低查找的时间复杂度,提高了查询效率。
- 高效的插入和删除操作:
- B+ 树的平衡性保证了树的高度不会过深,插入和删除操作的代价是比较稳定的,不会由于树的不平衡而导致性能下降。
- 通过调整分裂和合并操作,B+ 树可以保持平衡并具有高效的插入和删除性能。
一个表有索引说说它的查询过程?
查询过程大致步骤如下:
- 查询分析与优化:
- 解析查询语句:首先,数据库管理系统会对 SQL 查询语句进行语法分析和语义分析,理解查询的目的。
- 查询优化器:查询优化器会评估多种执行计划,决定最佳的查询方法。如果表上有相关索引,优化器会考虑使用索引来加速查询。
- 索引查找:数据库会根据查询条件从根节点开始,沿着 B+ 树的分支节点逐层定位到叶子节点,找到满足查询条件的索引记录。
- 回表(如果需要):索引通常只包含索引列和指向表中实际数据行的指针(或 ROWID)。如果查询需要的列不在索引中(即覆盖索引未被满足),数据库需要根据索引中的 ROWID 或指针回到原表中获取其他列的数据,这个过程称为“回表”查询。
- 数据返回:
- 筛选与排序:对于符合条件的行,数据库引擎可能还需要进行进一步的筛选(比如 WHERE 子句中的其他条件),以及按照 ORDER BY、GROUP BY 等进行排序操作。
- 结果集生成:最终,数据库将处理后的数据组织成查询结果集返回给用户。
如果要操作1千万条数据要注意什么问题?
操作 1 千万条数据时,需要性能问题和系统稳定性和安全问题,主要体现在以下几点:
- 性能优化:
- 索引优化:确保对经常查询的列建立合适的索引,以加速查询速度。但同时要避免过度索引,因为索引也会占用存储空间并影响写入性能。
- 分批处理:避免一次性加载或操作所有数据,可以将数据分成小批次进行处理,减少内存消耗和避免阻塞系统。
- 避免全表扫描:尽量避免使用会导致全表扫描的查询,如使用SELECT *或在没有索引的列上进行查询。
- 资源管理:
- 内存管理:监控和控制程序的内存使用,避免内存溢出。在处理大量数据时,合理分配内存,特别是进行排序、分组等操作时。
- 磁盘I/O优化:考虑到数据库操作可能引起的大量磁盘读写,优化磁盘I/O性能,比如使用SSD存储,调整文件系统缓存设置等。
- 数据库设计:
- 分区表:根据业务需求对大表进行水平或垂直分区,将数据分布在不同的物理位置,提升查询效率。
- 归档旧数据:定期归档或删除不再需要的历史数据,保持活跃数据集在一个可控的范围内。
- 查询优化:
- 避免复杂的JOIN操作:尽量减少或避免复杂的表连接,特别是对于千万级别的数据表,考虑是否可以通过预计算或使用汇总表来简化查询。
- 使用覆盖索引:确保查询只需要索引中的列,这样数据库可以直接从索引中返回数据而无需回表,提高查询速度。
- 事务管理:
- 合理使用事务:对于大量数据的插入、更新操作,适当使用事务以确保数据一致性,但要避免过大的事务,以免长时间锁定资源。
- 批量提交:在插入大量数据时,使用批量插入而不是单条插入,并在适当的时候提交事务,减少提交次数。
- 备份与恢复:
- 在进行大规模数据操作之前,确保有完整的数据备份,以防操作失误导致数据丢失。
课后思考
最后一个问题是“如何进行JVM调优?”,各位大佬觉得应该如何回答这个问题?
本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。