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) 的流程
计算 hash → 定位数组下标
比较:
若第一个节点就匹配 → 直接返回
若是树结构 → 红黑树查找
若是链表 → 顺序遍历比较 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.log、system.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"),hgetallList 快速列表(quicklist = ziplist + linkedlist) 消息队列、最新 N 条记录 lpush,rpop,lrangeSet 整数集合(intset)或哈希表 标签、共同好友、去重 sadd,sismember,sinterSorted Set (ZSet) 跳表(skiplist)+ 哈希表 排行榜、延迟队列、带权重队列 zadd,zrangeByScore,zrem⑥ Bitmap(位图)------超高效的布尔记录
**场景:**签到打卡、用户是否访问过、是否打开某个开关
⑦ HyperLogLog(基数统计)------极省空间的大规模去重统计
**场景:**UV 统计(独立访客数)、大规模去重计数
⑧ Geo(地理位置)------存储 & 查询地理坐标
**场景:**附近的人、附近的门店、骑手、地理围栏
Redis 内置:距离查询、范围查询。
Redis持久化策略
介绍一下什么是主主从模式、哨兵模式、集群模式吧
Redis的主从模式就是一个主节点带多个从节点,主节点写入数据会同步到从节点,这样既能读写分离提高性能,又能做数据备份。
哨兵模式是在主从基础上加几个哨兵节点,监控主节点状态,万一主节点挂了,哨兵会自动选一个从节点当新主节点,实现自动故障转移。
集群模式则是把数据分片存储到多个主从节点上,每个分片都有主从,不仅能自动故障转移,还能水平扩展,提升性能和存储能力。
MySQL
什么是MVCC
MVCC,全称是 Multi-Version Concurrency Control ,中文叫多版本并发控制。
简单来说,它是一种不用加锁 就能让多个事务并发读写数据的机制。
它的核心思想是:保存数据的多个版本,让读操作和写操作互不干扰。
比如你读数据时,看到的是你读那一刻的"快照",即使别人正在修改,也不会影响你。等你的事务结束前,看到的数据都是一样的,不会乱变。
它靠undo log存旧数据,再通过事务ID判断你能看到哪个版本。这样读和写互不干扰,不用加锁也能保证并发安全,特别适合高并发场景。
在MySQL的InnoDB里,它实现了"读已提交"和"可重复读"这两种隔离级别。
MVCC 的三个关键组件(InnoDB 实现)
隐藏列(DB_TRX_ID、DB_ROLL_PTR)
DB_TRX_ID:创建/修改该行的事务 ID
DB_ROLL_PTR:指向 Undo Log,用于回溯旧版本
Undo Log(回滚日志)
- 保存旧版本数据,实现历史版本回溯
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、接口注解)
② 异常没抛出或类型不对
③ 线程不一致(异步、多线程)
④ 传播行为导致的事务挂起
方法被自身调用(最常见的失效原因)
@Transactional 用在非 public 方法上
事务异常类型不对(未抛出 RuntimeException 或 Error)
默认情况下:
Spring 只对 RuntimeException、Error 回滚
对 受检异常(Checked Exception)不回滚
- 方法内部捕获了异常且未重新抛出。因为 Spring 感知不到异常,认为事务正常结束 → 不回滚
5. @Transactional 写在接口上而不是实现类上
Spring 默认使用 JDK 动态代理时:
注解写在 接口上 → 不生效
注解写在 实现类上 → 生效
若使用 CGLIB 代理则可正常。
- 事务传播行为导致覆盖
8使用多线程导致事务失效
事务绑定在 当前线程 上,切到新线程就没事务了。
spring bean的初始化和实例化的区别
维度 实例化(Instantiation) 初始化(Initialization) 本质 创建对象( new)让对象变得可用 时机 生命周期第一步 实例化之后 属性状态 未注入(null) 已注入(非 null) 是否可干预 可通过 InstantiationAwareBeanPostProcessor可通过 BeanPostProcessor、@PostConstruct等AOP 代理 尚未生成 在此阶段末尾生成 循环依赖 提前暴露原始对象 完整对象放入单例池
Mybatis
Mybatis二级缓存和一级缓存存在什么区别?
一级缓存是 SqlSession 级别的"会话内缓存",自动生效;二级缓存是 Mapper 级别的"应用级缓存",需手动开启,用于跨会话共享数据。
合理使用两级缓存,可显著减少数据库压力,但必须警惕数据一致性问题,尤其在写多读少或分布式场景下。
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)
- Bootstrap ClassLoader(C++ 实现,加载核心库如
✅ 双亲委派模型:防止核心类被篡改,保证类的唯一性。
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),每个方法调用创建一个帧 | 存放局部变量、操作数栈、动态链接、方法出口;可能发生 StackOverflowError 或 OutOfMemoryError |
| 本地方法栈(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,通过它可以完全理解线程池:
corePoolSize --- 核心线程数(长期存活)
maximumPoolSize --- 最大线程数
keepAliveTime --- 非核心线程的闲置存活时间
unit --- 时间单位
workQueue --- 任务队列
threadFactory --- 创建线程工厂,可以自定义线程名
handler(拒绝策略) --- 超出最大线程 + 队列时的处理策略
线程池的执行流程
先核心线程 → 再队列 → 再非核心线程 → 最后拒绝策略
具体流程:
若运行的线程数 < corePoolSize,创建核心线程执行任务
若核心线程已满,将任务放入队列
若队列也满,再创建非核心线程(直到 maximumPoolSize)
若线程数达到 maximum 且队列满,则执行拒绝策略
常用的拒绝策略
AbortPolicy(默认):抛异常
CallerRunsPolicy:由提交任务的线程自己执行(降低压力)
DiscardPolicy:直接丢弃
DiscardOldestPolicy:丢弃队列最老任务