高频提问部分

java基础

== 与 equals 有什么区别?

对于字符串变量来说,使用"=="和"equals"比较字符串时,其比较方法不同。对于字符串变量来说,使用"=="和"equals"比较字符串时,其比较方法不同。

对于非字符串变量来说,如果没有对equals()进行重写的话,"==" 和 "equals"方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。

  • ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
  • equals():比较的是两个字符串的内容,属于内容比较。

为什么要同时重写equals和hashcode

因为 HashMap / HashSet 等基于哈希的数据结构,判断两个对象是否"相等"要遵循 equals 与 hashCode 的一致性约定,否则会出现对象重复、查找失败等问题。

Java 明确规定:

只要两个对象 equals 相等,它们的 hashCode 必须一致。

但反过来不要求(哈希碰撞可以存在):

hashCode 相等,不一定 equals 相等。

HashMap的底层原理

HashMap 底层由 数组 + 链表 + 红黑树 组成,解决的是 哈希冲突与高效查询 的问题。

每个桶(bucket)可能是:

  • null

  • 链表(<=8 个节点)

  • 红黑树(>8 个节点)

树化阈值:8
退化链表阈值:6

目的:在高冲突场景下,把 O(n) 的链表查询优化成 O(log n) 的红黑树查询。

put(K,V) 的流程

① 对 key 求 hash hash = (h = key.hashCode()) ^ (h >>> 16)扰动函数降低 hash 冲突。

② 计算数组下标 index = hash & (n - 1)利用位运算快速取模(数组长度必须是 2 的幂)。

③ 写入节点

  • 若桶为空:直接放入

  • 若 key 相同:覆盖旧值

  • 若冲突:

    • 链表插入

    • 若链表长度 > 8 且数组长度 >= 64 → 红黑树化

get(K) 的流程

  1. 计算 hash → 定位数组下标

  2. 比较:

    • 若第一个节点就匹配 → 直接返回

    • 若是树结构 → 红黑树查找

    • 若是链表 → 顺序遍历比较 equals()

时间复杂度:

  • 链表:O(n)

  • 树:O(log n)

  • 理想状态:O(1)

HashMap 是线程不安全的?为什么?

JDK 1.7 :链表采用头插法,多线程扩容可能链表成环 → get 死循环。

JDK 1.8 :改为尾插法,减少环风险,但依然 不是线程安全

并发下可能出现:

  • 数据丢失(put put 覆盖)

  • 数据不一致

  • 扩容时出现读写错误

解决方案

  • ConcurrentHashMap

  • Collections.synchronizedMap

消息队列

RabbitMQ 的"刷盘机制"了解吗?持久化什么时候写入磁盘?

RabbitMQ的刷盘机制其实就是把内存里的消息写到磁盘,保证数据不丢。它有同步和异步两种方式,同步就是等数据写完磁盘才返回确认,安全但慢;异步是先返回确认,再慢慢写磁盘,快但可能丢数据。而且它还支持惰性队列,消息直接怼磁盘,省内存但会慢点。

RabbitMQ默认是异步刷盘。它会先把消息写到内存或PageCache,攒够一批再刷到磁盘,这样吞吐量更高。
RabbitMQ 的持久化并不会实时写磁盘,而是先写入操作系统的 PageCache,再由 OS 异步刷盘。
正常情况下刷盘由内核在内存压力或定时任务时触发;
只有在 Publisher Confirm 模式下,RabbitMQ 才会调用 fsync 等确保数据真正落盘后才给生产端 ack。
这样兼顾了性能和可靠性。

RabbitMQ 的死信队列是什么?什么时候会进入死信?

RabbitMQ 的死信队列(Dead Letter Exchange, DLX) 是一种非常重要的异常消息处理机制,用于捕获那些无法被正常消费的消息,避免消息丢失,并支持后续的人工干预或自动重试。

什么是死信?什么情况下消息会变成"死信"?

| 死信产生原因 | 说明 |
| 1. 消息被拒绝(Reject/Nack)且 requeue=false | 消费者明确表示"不要这条消息,也不要重新入队" |
| 2. 消息 TTL(Time-To-Live)过期 | 消息在队列中存活时间超过设定的 TTL |

3. 队列达到最大长度限制(x-max-length) 队列满了,新消息进来时,最早的消息被"挤出去"成为死信

✅ 注意:只有普通队列中的消息才可能变成死信,死信队列本身的消息不会再进入另一个死信队列。

RabbitMQ 的消息队列模型

| 模式 | 说明 | 对应 Exchange |
| 1. Hello World(简单模式) | 1 Producer → 1 Queue → 1 Consumer | Direct |
| 2. Work Queues(工作队列) | 1 Queue → 多个 Consumer(轮询消费) | Direct |
| 3. Publish/Subscribe(发布订阅) | 广播消息给所有消费者 | Fanout |
| 4. Routing(路由模式) | 按 Routing Key 精确路由 | Direct |
| 5. Topics(主题模式) | 按通配符路由 | Topic |

6. RPC(远程调用) 请求-响应模型,使用临时回调队列 Direct

交换机的类型有以下三种:

Fanout:广播交换机会将接收到的消息广播到每一个跟其绑定的 queue ,所以也叫广播模式

  • 路由规则 :忽略 Routing Key,广播到所有绑定的 Queue
  • 适用场景:通知、日志广播、配置更新
  • 特点:最快,无需匹配计算

Direct:定向:Direct 交换机会将接收到的消息根据规则路由到指定的队列,被称为定向路由

  • 每一个 Queue 都与 Exchange 设置一个 bindingKey
  • 发布者发送消息时,指定消息的 RoutingKey
  • Exchange 将消息路由到 bindingKey 与消息 routingKey 一致的队列

需要注意的是:同一个队列可以绑定多个 bindingKey ,如果有多个队列绑定了同一个 bindingKey 就可以实现类似于 Fanout 交换机的效果。

Topic:话题:

  • 路由规则 :支持通配符匹配(* 匹配一个词,# 匹配多个词)
  • Routing Key 格式word1.word2.word3...
  • 示例
    • 绑定 key:*.error.#
    • 匹配:user.error.logsystem.error.db.connection
  • 适用场景:多维度消息分类(如日志级别+模块)

如何保证消息不被重复消费?(幂等性)

幂等性核心思想:同一个操作,无论执行多少次,结果都一致。

实现关键:用唯一标识 + 状态记录,判断是否已处理过。

  • 唯一标识(幂等键):客户端为每个请求生成全局唯一ID(如 UUID、业务主键),服务端校验该ID是否已处理,适用场景接口调用、消息消费等。
  • 数据库事务 + 乐观锁:通过版本号或状态字段控制并发更新,确保多次更新等同于单次操作,适用场景数据库记录更新(如余额扣减、订单状态变更)。
  • 数据库唯一约束:利用数据库唯一索引防止重复数据写入,适用场景数据插入场景(如订单创建)。
  • 分布式锁:通过锁机制保证同一时刻仅有一个请求执行关键操作,适用场景高并发下的资源抢夺(如秒杀)。
  • 消息去重:消息队列生产者为每条消息生成唯一的消息 ID,消费者在处理消息前,先检查该消息 ID 是否已经处理过,如果已经处理过则丢弃该消息。

RabbitMQ如何实现高效读写

底层高效读写机制

基于 Erlang 的轻量级进程模型

  • 每个连接、每个队列、每个消费者都由独立的轻量级 Erlang 进程处理,避免线程竞争。
  • 进程间通过消息传递通信,无锁设计,减少上下文切换开销。

内存 + 磁盘混合存储策略

  • 热数据常驻内存:活跃队列的消息优先存于内存,读写极快。
  • 冷数据刷盘持久化
    • 当内存压力大时,自动将不活跃消息写入磁盘(queue_index_embed_msgs_below 控制阈值)。
    • 使用 顺序写日志(msg_store)索引文件(queue_index),减少随机 I/O。
  • 读取时按需加载:消费者拉取消息时,若在磁盘则异步加载回内存。

提升读写效率的关键优化点 1. 合理使用持久化(权衡性能与可靠)

配置 性能影响 建议
durable=true(队列持久化) ⚠️ 中等 必须开启(否则重启丢队列)
delivery_mode=2(消息持久化) ⚠️⚠️ 较大 仅对关键消息开启,非关键消息可设为非持久化
禁用持久化 ✅ 极高吞吐 仅用于临时任务、可丢失场景

Redis:

Redis常用的数据结构有那些?

Redis 提供 5 大基础数据结构 + 3 大高级结构:

数据结构 底层实现(简要) 典型应用场景 Java 中常用命令(Lettuce / RedisTemplate)
String 简单动态字符串(SDS) 缓存对象、计数器、分布式锁 set(key, value), get(key), incr(key)
Hash 哈希表(ziplist / hashtable) 存储对象(如用户信息) hset(user:1001, name, "Tom"), hgetall
List 快速列表(quicklist = ziplist + linkedlist) 消息队列、最新 N 条记录 lpush, rpop, lrange
Set 整数集合(intset)或哈希表 标签、共同好友、去重 sadd, sismember, sinter
Sorted Set (ZSet) 跳表(skiplist)+ 哈希表 排行榜、延迟队列、带权重队列 zadd, zrangeByScore, zrem

⑥ Bitmap(位图)------超高效的布尔记录

**场景:**签到打卡、用户是否访问过、是否打开某个开关

⑦ HyperLogLog(基数统计)------极省空间的大规模去重统计

**场景:**UV 统计(独立访客数)、大规模去重计数

⑧ Geo(地理位置)------存储 & 查询地理坐标

**场景:**附近的人、附近的门店、骑手、地理围栏

Redis 内置:距离查询、范围查询。

Redis持久化策略

Redis的持久化机制_rdb-CSDN博客

什么是RDB和AOF_rdb aof-CSDN博客

介绍一下什么是主主从模式、哨兵模式、集群模式吧

Redis的主从模式就是一个主节点带多个从节点,主节点写入数据会同步到从节点,这样既能读写分离提高性能,又能做数据备份。

哨兵模式是在主从基础上加几个哨兵节点,监控主节点状态,万一主节点挂了,哨兵会自动选一个从节点当新主节点,实现自动故障转移。

集群模式则是把数据分片存储到多个主从节点上,每个分片都有主从,不仅能自动故障转移,还能水平扩展,提升性能和存储能力。

MySQL

什么是MVCC

MVCC,全称是 Multi-Version Concurrency Control ,中文叫多版本并发控制

简单来说,它是一种不用加锁 就能让多个事务并发读写数据的机制。

它的核心思想是:保存数据的多个版本,让读操作和写操作互不干扰。

比如你读数据时,看到的是你读那一刻的"快照",即使别人正在修改,也不会影响你。等你的事务结束前,看到的数据都是一样的,不会乱变。

它靠undo log存旧数据,再通过事务ID判断你能看到哪个版本。这样读和写互不干扰,不用加锁也能保证并发安全,特别适合高并发场景。

在MySQL的InnoDB里,它实现了"读已提交"和"可重复读"这两种隔离级别。
MVCC 的三个关键组件(InnoDB 实现)

  1. 隐藏列(DB_TRX_ID、DB_ROLL_PTR)

    • DB_TRX_ID:创建/修改该行的事务 ID

    • DB_ROLL_PTR:指向 Undo Log,用于回溯旧版本

  2. Undo Log(回滚日志)

    • 保存旧版本数据,实现历史版本回溯
  3. Read View(读视图)

    • 当前事务可见哪些版本的数据

    • 包含:

      • 当前未提交事务列表

      • 最小事务 ID

      • 最大事务 ID

spring

说一下,spring、springMVC、spring boot、springclould的区别和联系

Spring Framework: java开发的"基础框架",Spring是整个生态的基石,它提供了一套强大的企业级应用工具。

核心功能是: IoC(控制反转) 和 **AOP(面向切面编程),帮助你掌管对象的生命周期和处理日志、事务等公共功能
缺点:**配置相对繁琐,需要手动的编写很多XML配置文件或java Config代码。

Spring MVC: 构建于 Spring 之上,专注于 Web 层 的 MVC 框架,用于开发 Web 应用程序。

Spring Boot: 构建于 Spring 之上,旨在 简化 Spring 应用的搭建和开发,其核心是"约定大于配置"和自动装配。

Spring Cloud: 构建于 Spring Boot 之上,提供了一套完整的 分布式系统/微服务架构 的解决方案。
Spring Boot

  • 定位: Spring 的上层框架 ,其核心目标是 让 Spring 应用变得更快、更简单

  • 核心价值:

    • 自动配置: 根据项目中的 Jar 包依赖,自动推断并配置 Spring 应用。例如,引入了 spring-boot-starter-web,它就自动配置好内嵌 Tomcat 和 Spring MVC。

    • 起步依赖: 将常用功能聚合为一组依赖(starter),简化 Maven/Gradle 配置。

    • 内嵌容器: 将 Web 服务器(Tomcat, Jetty)内置于应用,使应用可独立运行为 Jar 包,无需部署到外部容器。

    • 生产就绪功能: 提供监控(Actuator)、健康检查、外部化配置等开箱即用的特性。

Spring Cloud

  • 定位: 基于 Spring Boot分布式系统/微服务架构的一站式解决方案

  • 核心价值: 提供分布式环境下所需的一系列模式的实现。

    • 服务治理: 服务发现与注册(Eureka, Nacos)、配置中心(Config, Nacos)。

    • 客户端负载均衡: Ribbon。

    • 服务调用: OpenFeign(声明式的 REST 客户端)。

    • 熔断与降级: Hystrix, Sentinel。

    • 网关: Zuul, Gateway。

    • 链路追踪: Sleuth + Zipkin。

Spring的事务什么情况下会失效?

Spring 的事务基于 AOP 代理 + @Transactional。失效的本质就是:

代理没有生效 或 事务边界没有创建。

Spring 事务失效主要集中在:

没有走代理(自调用、非 public、final、接口注解)

异常没抛出或类型不对

线程不一致(异步、多线程)

传播行为导致的事务挂起

  1. 方法被自身调用(最常见的失效原因)

  2. @Transactional 用在非 public 方法上

  3. 事务异常类型不对(未抛出 RuntimeException 或 Error)

默认情况下:

  • Spring 只对 RuntimeException、Error 回滚

  • 受检异常(Checked Exception)不回滚

  1. 方法内部捕获了异常且未重新抛出。因为 Spring 感知不到异常,认为事务正常结束 → 不回滚

5. @Transactional 写在接口上而不是实现类上

Spring 默认使用 JDK 动态代理时:

  • 注解写在 接口上 → 不生效

  • 注解写在 实现类上 → 生效

若使用 CGLIB 代理则可正常。

  1. 事务传播行为导致覆盖

8使用多线程导致事务失效

事务绑定在 当前线程 上,切到新线程就没事务了。

spring bean的初始化和实例化的区别

维度 实例化(Instantiation) 初始化(Initialization)
本质 创建对象(new 让对象变得可用
时机 生命周期第一步 实例化之后
属性状态 未注入(null) 已注入(非 null)
是否可干预 可通过 InstantiationAwareBeanPostProcessor 可通过 BeanPostProcessor@PostConstruct
AOP 代理 尚未生成 在此阶段末尾生成
循环依赖 提前暴露原始对象 完整对象放入单例池

Mybatis

Mybatis二级缓存和一级缓存存在什么区别?

一级缓存是 SqlSession 级别的"会话内缓存",自动生效;二级缓存是 Mapper 级别的"应用级缓存",需手动开启,用于跨会话共享数据。

合理使用两级缓存,可显著减少数据库压力,但必须警惕数据一致性问题,尤其在写多读少或分布式场景下。

Mybatis一级缓存和二级缓存-CSDN博客

JVM

JVM的组成

复制代码
┌───────────────────────────────────────┐
│            Class Loader               │ ← 加载 .class 文件
└──────────────────┬────────────────────┘
                   ↓
┌───────────────────────────────────────┐
│        Runtime Data Areas              │ ← 运行时数据区(内存模型)
│  ┌─────────────┐  ┌─────────────────┐ │
│  │ Method Area │  │   Heap          │ │
│  └─────────────┘  └─────────────────┘ │
│  ┌─────────────┐  ┌─────────────────┐ │
│  │  PC Register│  │ Java Stack      │ │
│  └─────────────┘  └─────────────────┘ │
│  ┌─────────────────────────────────┐  │
│  │     Native Method Stack         │  │
│  └─────────────────────────────────┘  │
└──────────────────┬────────────────────┘
                   ↓
┌───────────────────────────────────────┐
│        Execution Engine                │ ← 执行引擎
│  ┌─────────────┐  ┌─────────────────┐ │
│  │ Interpreter │  │ JIT Compiler    │ │
│  └─────────────┘  └─────────────────┘ │
│           ┌─────────────────┐         │
│           │ Garbage Collector│        │
│           └─────────────────┘         │
└──────────────────┬────────────────────┘
                   ↓
┌───────────────────────────────────────┐
│       Native Method Interface (JNI)    │ ← 调用本地方法(C/C++)
└───────────────────────────────────────┘

JVM 的核心组成 = 类加载器 + 运行时数据区 + 执行引擎 + 本地接口

JVM 由类装载子系统、运行时数据区(含堆/栈/方法区等)、执行引擎三大核心组成,并通过 JNI 与本地库交互。

其中运行时数据区是重点,包括线程私有的程序计数器、Java 栈、本地方法栈,线程共享的堆、方法区、运行时常量池。

1. 类加载器子系统(Class Loader Subsystem)

负责将 .class 字节码文件加载到内存,并生成对应的 Class 对象。

  • 加载过程

    • 加载(Loading):查找并导入字节码文件。
    • 链接(Linking)
      • 验证(Verification):确保字节码安全合法;
      • 准备(Preparation):为静态变量分配内存并设默认值;
      • 解析(Resolution):将符号引用转为直接引用。
    • 初始化(Initialization) :执行 <clinit> 方法(静态代码块和静态变量赋值)。
  • 类加载器类型

    • Bootstrap ClassLoader(C++ 实现,加载核心库如 rt.jar
    • Extension ClassLoader(加载扩展库)
    • Application ClassLoader(加载用户类路径下的类)
    • 自定义 ClassLoader(继承 ClassLoader

双亲委派模型:防止核心类被篡改,保证类的唯一性。

2. 运行时数据区(Runtime Data Areas) ------ JVM 内存模型

这是 JVM 管理内存的核心区域,分为线程共享线程私有两部分:

🔹线程共享区域(所有线程共用)

区域 作用 特点
堆(Heap) 存放对象实例、数组 GC 主战场,可设置大小(-Xmx, -Xms
方法区(Method Area) 存储类信息、常量、静态变量、JIT 编译后的代码 JDK 8 后由 元空间(Metaspace) 实现,使用本地内存

📌 注意:字符串常量池在 JDK 7+ 也移到了堆中。

🔹 线程私有区域(每个线程独立)

区域 作用 特点
程序计数器(PC Register) 记录当前线程执行的字节码指令地址 唯一不会发生 OOM 的区域
Java 虚拟机栈(Java Stack) 存储栈帧(Frame),每个方法调用创建一个帧 存放局部变量、操作数栈、动态链接、方法出口;可能发生 StackOverflowErrorOutOfMemoryError
本地方法栈(Native Method Stack) 为 Native 方法(如 JNI 调用 C/C++)服务 有些 JVM 将其与 Java 栈合并
3、**执行引擎(Execution Engine)**负责执行字节码指令。
  • 解释器(Interpreter):逐条解释执行字节码,启动快但效率低。
  • JIT 编译器(Just-In-Time Compiler)
    • 将热点代码(频繁执行的方法/循环)编译为本地机器码
    • 存储在代码缓存(Code Cache) 中,后续直接执行;
    • 分为 C1(Client Compiler,快速编译)和 C2(Server Compiler,重度优化)。
  • 垃圾回收器(Garbage Collector)
    • 自动回收堆中不再使用的对象;
    • 常见 GC:Serial、Parallel、CMS、G1、ZGC、Shenandoah。

💡 混合模式(-Xmixed):JVM 默认先解释执行,再对热点代码 JIT 编译。

4. 本地方法接口(JNI, Java Native Interface)

允许 Java 代码调用其他语言(如 C/C++)编写的本地方法。

  • 本地方法存储在本地方法栈中;
  • 打破了 Java 的平台无关性,但用于性能关键或系统级操作。

说说你对线程池的理解

线程池的核心价值是 复用线程、减少频繁创建销毁带来的开销,提高系统整体吞吐量

主要解决三个问题:

  • 降低资源消耗:创建线程开销大(栈内存 + 系统调度),线程池统一管理。

  • 提高响应速度:任务来了就能复用已有线程执行,而不是等新线程创建。

  • 统一管理线程:可控制线程数量、队列长度、拒绝策略,避免线程无限增长造成 OOM。

线程池的核心参数(ThreadPoolExecutor 的 7 大核心参数)

Java 最本质就是 ThreadPoolExecutor,通过它可以完全理解线程池:

  1. corePoolSize --- 核心线程数(长期存活)

  2. maximumPoolSize --- 最大线程数

  3. keepAliveTime --- 非核心线程的闲置存活时间

  4. unit --- 时间单位

  5. workQueue --- 任务队列

  6. threadFactory --- 创建线程工厂,可以自定义线程名

  7. handler(拒绝策略) --- 超出最大线程 + 队列时的处理策略

线程池的执行流程

先核心线程 → 再队列 → 再非核心线程 → 最后拒绝策略

具体流程:

  1. 若运行的线程数 < corePoolSize,创建核心线程执行任务

  2. 若核心线程已满,将任务放入队列

  3. 若队列也满,再创建非核心线程(直到 maximumPoolSize)

  4. 若线程数达到 maximum 且队列满,则执行拒绝策略

常用的拒绝策略

  1. AbortPolicy(默认):抛异常

  2. CallerRunsPolicy:由提交任务的线程自己执行(降低压力)

  3. DiscardPolicy:直接丢弃

  4. DiscardOldestPolicy:丢弃队列最老任务

相关推荐
沐森7 小时前
面向 Trait 编程 (Trait-Driven Design)
后端
深蓝电商API7 小时前
企业级爬虫架构设计:任务调度、容错、重试、降重
开发语言·爬虫·ruby
芝麻开门-新起点7 小时前
第13-1章 Python地理空间开发
开发语言·python
Dwzun7 小时前
基于SpringBoot+Vue的二手书籍交易平台系统【附源码+文档+部署视频+讲解)
java·vue.js·spring boot·后端·spring·计算机毕业设计
期待のcode7 小时前
Wrapper体系中的condition参数
java·spring boot·后端·mybatis
肥大毛7 小时前
C++入门学习---结构体
开发语言·c++·学习
小明记账簿7 小时前
JavaScript浮点数精度问题及解决方案
开发语言·javascript·ecmascript
雨中飘荡的记忆8 小时前
Spring状态机深度解析
java·后端·spring
南棱笑笑生8 小时前
20251213给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时适配type-C0
linux·c语言·开发语言·rockchip