跳到主要内容

Java 面试

Java 基础

Java 虚拟机

Java 多线程

创建一个线程时底层发生了什么?

在 Java 中,创建一个线程时,底层发生了以下几个主要步骤:

  1. 分配线程栈:线程对象被创建后,Java 虚拟机会为该线程分配一个独立的线程栈(Thread Stack),用于存储该线程的方法调用、局部变量等信息。
  2. 初始化线程属性:设置线程的属性,例如优先级、守护线程标志等。
  3. 调用线程的 start() 方法:当调用线程对象的 start() 方法时,会触发 Java 虚拟机调用 run() 方法,并启动线程的执行。
  4. 启动线程执行:Java 虚拟机会在后台创建并启动一个操作系统线程来执行 Java 线程,每个 Java 线程对应一个底层的操作系统线程。
  5. 线程的调度和执行:操作系统负责线程的调度,按照线程优先级、时间片轮转等算法来确定线程的执行顺序,线程开始执行其业务方法。
  6. 线程执行结束:当线程的业务方法执行完毕或线程抛出异常导致执行终止时,线程进入终止状态。

说说线程的生命周期?

线程生命周期总共有以下 6 种:

  1. NEW(新建状态):new Thread() 时线程的状态。
  2. RUNNABLE(可运行/运行状态):调用 start() 方法后的状态。
  3. BLOCKED(阻塞状态):调用了 synchronized 加锁之后的状态。获得锁之后就从 BLOCKED 状态变成了 RUNNABLE 状态。
  4. WAITING(无时限等待状态):调用了 wait() 方法之后会进入此状态。
  5. TIMED_WAITING(有时限等待状态):调用了 sleep(long millis) 方法之后会进入此状态。
  6. TERMINATED(终止状态):线程任务执行完成之后就变成此状态。 线程状态的转换如下图所示:

使用线程池时核心线程数和最大线程数如何设计?

核心线程数设计分为以下两种情况:

  1. 计算型任务:根据实际业务场景设置,参考值为:CPU 核数+1。
  2. IO 型任务:根据实际业务场景设置,参考值为:2*CPU 核数+1。

    PS:为什么要 CPU 核数+1,而不是 CPU 核数?多出来的一个线程可用于更加平滑的线程替补,例如有现成执行完,或休眠、中断等,多出来的一个线程就可以尽快的进行补充,避免了 CPU 空闲。 如果,任务量波动不大,可以将最大线程数设置和核心线程数一样的数量,避免临时线程创建和销毁的性能开销,如果波动比较大,可根据实际业务场景设置,参考值为 2 倍的核心线程数。

Java 微服务

说说你都用过哪些微服务组件?

微服务常见组件如下:

  1. Nacos:注册中心和配置中心。
    1. 注册中心:负责维护微服务实例的注册信息,包括服务地址和服务状态。服务启动时向注册中心注册自身,服务消费者通过注册中心发现服务提供者的地址,实现服务间的动态发现与路由。
    2. 配置中心:集中管理微服务应用的配置信息,支持动态配置更新,使得微服务可以在运行时获取或更新配置,便于配置的统一管理和版本控制。
  2. Spring Cloud OpenFeign:RESTful(基于标准的 HTTP 协议和 URI(Uniform Resource Identifier)的一组约束和原则)通讯,负责微服务之间的调用。
  3. Spring Cloud LoadBalancer:负载均衡器(客户端)负责微服务调用时节点的选取。
  4. Spring Cloud Gateway:网关,微服务架构的入口,提供统一的接口访问入口,处理所有进来的请求,包括路由转发、协议转换、安全认证(如OAuth)、速率限制、负载均衡等,对外屏蔽微服务细节。
  5. Sentinel:限流、熔断降级,防止服务雪崩效应,增强系统的稳定性和韧性。
  6. Seata:分布式事务,保证微服务间一组执行方法的原子性(要么一起执行成功,要么一起执行失败)。
  7. Skywalking:分布式链路追踪,记录微服务调用日志,监控系统和方便排查问题。

Java 组件工具

Dubbo

讲一下Dubbo运行原理?

Dubbo 是一款高性能、轻量级的开源 RPC(远程过程调用)框架,主要用于构建分布式服务和微服务架构。 要说 Dubbo 运行流程就不得不先来了解一下 Dubbo 的核心组件了,因为 Dubbo 的交互流程是和核心组件息息相关的。 Dubbo 核心组件有以下几个:

  • 服务提供者(Provider):暴露服务的应用,通过 Dubbo 框架将自身的服务接口及实现注册到注册中心。
  • 服务消费者(Consumer):调用远程服务的应用,从注册中心订阅所需的服务,然后通过远程调用消费服务。
  • 注册中心(Registry):集中管理服务的地址信息,服务提供者和服务消费者均在此注册或订阅服务信息。常见的注册中心有 ZooKeeper、Nacos 等。 Dubbo 运行流程如下图所示: 它的执行流程如下:
  1. 服务提供者会将实例(URL 地址)注册到注册中心,注册中心负责对数据进行聚合(健康检测)。
  2. 消费者从注册中心读取地址列表并订阅变更,每当地址列表发生变化,注册中心将最新的列表通知到所有订阅的消费者实例。
  3. 消费者得到服务实例之后,通过 Dubbo 内置的负载均衡策略,选择其中的一个节点,之后使用 RPC 的方式与服务提供者建立连接,并进行通讯和服务调用。 更详细的调用流程如下: image.png

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)内置了如下负载均衡算法如下:

  1. Weighted Random LoadBalance(加权随机):默认负载均衡算法,默认权重相同。按权重设置随机概率。缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  2. RoundRobin LoadBalance(加权轮询):借鉴于 Nginx 的平滑加权轮询算法,默认权重相同,按公约后的权重设置轮询比率,循环调用节点。缺点:同样存在慢的提供者累积请求的问题。
  3. LeastActive LoadBalance(最少活跃优先+加权随机):背后是能者多劳的思想,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。
  4. Shortest-Response LoadBalance(最短响应优先+加权随机):更加关注响应速度,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机。使得响应时间越快的提供者,处理更多的请求。缺点:可能会造成流量过于集中于高性能节点的问题。
  5. ConsistentHash LoadBalance(一致性哈希):确定的入参,确定的提供者,适用于有状态请求。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  6. P2C LoadBalance(随机选择两个节点+连接数较小):随机选择两个节点后,继续选择“连接数”较小的那个节点。对于每次调用,从可用的 provider 列表中做两次随机选择,选出两个节点 providerA 和 providerB,比较 providerA 和 providerB 两个节点,选择其“当前正在处理的连接数”较小的那个节点。
  7. Adaptive LoadBalance(自适应负载均衡):在 P2C 算法基础上,选择二者中 load 最小的那个节点,是一种能根据后端实例负载自动调整流量分布的算法实现,它总是尝试将请求转发到负载最小的节点。

RPC和HTTP有什么区别?

RPC(Remote Procedure Call,远程过程调用)和 HTTP(Hypertext Transfer Protocol,超文本传输协议)都是用于服务间通讯的,它们主要区别如下:

  1. 概念和使用场景不同
    • RPC:RPC 是一种通信模式,允许一个程序在另一个地址空间上执行远程计算过程,使得客户端调用远程服务就像调用本地方法一样。
    • HTTP:HTTP 是一个应用层协议,用于在客户端和服务器之间传输文本、图像、视频等超媒体资源,通常用于 Web 应用之间的通信。
  2. 传输数据不同
    • RPC:RPC 通常基于二进制数据传输,可以使用更高效的序列化方式(如 Protobuf、Thrift)进行数据交换。
    • HTTP:HTTP 使用文本协议,请求和响应数据通常是基于文本格式(如 JSON、XML)进行传输。
  3. 传输效率与性能不同
    • RPC:因为 RPC 通常使用更高效的二进制序列化(如 Protobuf、Thrift),减少了数据传输的体积,且由于其针对性的设计,往往在性能上更为优越,特别是在大量小数据包的传输场景。
    • HTTP:传统上使用文本格式如 JSON 进行数据交换,这可能导致更大的数据包和更多的序列化/反序列化开销,但在 HTTP/2 中引入了头部压缩和多路复用,提升了效率。

让你设计一个RPC框架,如何考虑数据序列化问题?

序列化是将对象转换为字节流的过程,可以用于数据持久化、数据传输等场景。序列化的主要目的是将对象在内存中的状态转换为可存储或传输的形式。 数据序列化需要考虑的以下问题:

  1. 性能问题:选择高性能的序列化库至关重要。二进制序列化(如 Protocol Buffers, Apache Thrift, FlatBuffers)通常比文本格式(如 JSON、XML)更高效,因为它们占用的空间小,序列化和反序列化的速度更快。对于高性能要求的场景,应优先考虑这些二进制格式。
  2. 安全性:在序列化过程中,应考虑数据的安全性,避免敏感信息的泄露。可以采用加密序列化内容、过滤敏感字段或使用安全的传输层协议(如 TLS/SSL)来增加安全性。
  3. 兼容性:良好的版本兼容性是长期维护 RPC 框架的关键。设计时要考虑向前和向后兼容,即新老版本的序列化库应能互相理解和处理对方生成的数据格式。可以采用预留字段、版本标识符等机制来支持这一点。
  4. 跨语言支持:RPC 框架往往需要支持多种编程语言,因此选择一种跨语言的序列化方案是必要的。Protocol Buffers、Apache Thrift、Avro 等都是很好的选择,它们提供了多种语言的编解码库。
  5. 可扩展性:设计时应考虑到未来可能增加的数据结构和字段,序列化方案应易于扩展,支持动态字段、自定义类型等特性。
  6. 可配置性:允许用户根据实际需求选择或切换序列化策略。例如,对于对性能要求极高的场景,用户可以选择最高效的序列化方式;而对于调试或日志记录,可能会偏好人类可读性更好的格式。
  7. 异常处理:在序列化或反序列化过程中可能会遇到错误(如数据损坏、不兼容的版本等)。框架应能优雅地处理这些异常,并提供清晰的错误信息,帮助开发者诊断问题。