问题 1:介绍一下 HashMap
答案:HashMap 是 Java 中基于哈希表实现的 Map 接口实现类,用于存储键值对,核心特性如下:
- 底层结构(JDK1.8):数组(哈希桶)+ 链表(解决哈希冲突)+ 红黑树(链表长度≥8 且数组长度≥64 时转换);
- 核心规则:允许
null键(哈希值固定为 0)和null值,无序,非线程安全; - 哈希计算:通过
key.hashCode() ^ (key.hashCode() >>> 16)混合高低位,数组下标 = 哈希值 & (数组长度 - 1); - 扩容机制:初始容量 16,负载因子 0.75,容量达阈值时扩容为 2 倍,JDK1.8 扩容无需重新计算哈希,仅按位判断位置;
- 遍历特性:快速失败(fail-fast),遍历中修改结构会抛出
ConcurrentModificationException。
问题 2:HashMap 是不是线程安全的?
答案:不是线程安全的。
-
线程不安全场景:
- 多线程扩容时,JDK1.7 头插法易形成环形链表导致死循环,JDK1.8 虽解决死循环,但仍会出现值覆盖、size 计数不准;
- 多线程
put()会覆盖彼此写入的值; - 遍历过程中修改数据触发快速失败。
-
线程安全替代方案:
ConcurrentHashMap(JDK1.7 分段锁,JDK1.8 CAS+Synchronized,效率高);Hashtable(全表加锁,效率低);Collections.synchronizedMap(new HashMap<>())(全局锁,简单场景可用)。
问题 3:volatile 描述下,如何保证可见性的?
答案:volatile 是 Java 轻量级同步机制,保证变量可见性(多线程间变量值实时同步),核心依赖内存屏障 + CPU 缓存一致性协议(MESI) :
- 写操作:对 volatile 变量写时,JVM 加写内存屏障,强制将变量从 CPU 缓存刷回主内存;
- 读操作:对 volatile 变量读时,JVM 加读内存屏障,强制从主内存加载变量,清空 CPU 缓存旧值;
- MESI 协议:多核 CPU 中,某核心修改主内存变量后,通知其他核心失效该变量缓存,确保所有核心读取最新值。补充:volatile 不保证原子性(如
i++仍需加锁 / AtomicInteger),仅保证可见性和禁止指令重排序。
问题 4:RabbitMQ 如何保证消息不丢失?
答案:核心是 “生产端 + 服务端 + 消费端 + 集群” 四层防护,确保消息全链路不丢失:
- 生产端:开启生产者确认(Publisher Confirm),异步监听确认结果,失败则重试 / 记录日志;
- 服务端:交换机、队列、消息均开启持久化(durable=true),避免服务宕机丢失消息;
- 消费端:关闭自动确认(autoAck=false),业务处理完成后手动
basicAck(),失败则basicNack()转入死信队列; - 集群层:开启镜像队列(ha-mode=all),所有节点同步队列数据,避免单节点宕机丢失。
问题 5:如何优雅的关闭线程池?
答案:核心原则:先拒绝新任务 → 等待已提交任务执行完成 → 超时强制关闭,避免任务丢失,步骤如下:
- 调用
shutdown():拒绝新任务,等待已提交(含队列中)任务执行完成; - 调用
awaitTermination(timeout, unit):阻塞等待线程池关闭,超时返回 false; - 超时未关闭则调用
shutdownNow():中断正在执行的线程,返回未执行任务列表; - 捕获
InterruptedException:若线程被中断,再次调用shutdownNow(),并恢复中断状态。核心代码示例:
java运行
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
System.err.println("线程池关闭失败");
}
}
问题 6:介绍下 BIO / NIO
答案:
| 特性 | BIO(同步阻塞 IO) | NIO(同步非阻塞 IO) |
|---|---|---|
| 核心模型 | 阻塞式,一个连接一个线程 | 非阻塞式,Reactor 事件驱动模型 |
| 关键组件 | Socket、ServerSocket | Channel、Buffer、Selector |
| 连接处理 | 线程阻塞等待数据,线程池易打满 | Selector 监听多 Channel 事件,单线程处理多连接 |
| 读写方式 | 流式读写(一次读一个字节) | 缓冲区批量读写 |
| 性能 | 高并发下线程切换开销大 | 低开销,支持百万级连接 |
| 适用场景 | 连接数少、短连接(如简单 HTTP) | 连接数多、长连接(如 Netty、Redis) |
问题 7:Spring Cloud Gateway 底层用的是什么模型?
答案:Spring Cloud Gateway 底层基于 Netty + Reactor 响应式模型(NIO) :
- 网络层:基于 Netty 的 NIO 模型(Epoll 多路复用),非阻塞 IO 处理网络请求;
- 编程层:基于 Reactor 3(Flux/Mono)响应式编程,异步非阻塞处理请求链路;
- 核心优势:相比 Zuul 1.x(Servlet/BIO),性能提升 3-5 倍,支持动态路由、流式处理。
问题 8:一个请求走到网关,操作系统经历了什么?
答案:以 Spring Cloud Gateway(Netty)为例,操作系统层面流程:
- TCP 三次握手:客户端发起 SYN → 网关内核 TCP 栈回应 SYN+ACK → 客户端 ACK,建立连接;
- Socket 监听:网关绑定端口,操作系统创建
listen socket处于 LISTEN 状态; - 连接接收:内核将新连接从半连接队列移到全连接队列,Netty 通过
accept()获取连接; - IO 多路复用:Netty 的 Selector 调用
epoll_ctl注册 Channel 读 / 写事件; - 数据读取:网卡数据 → 内核缓冲区 → 用户态(Netty ByteBuf);
- 请求处理:Netty EventLoop 处理请求,经过 Filter 链、路由转发;
- 响应返回:数据从用户态 → 内核缓冲区 → 网卡发送给客户端;
- 可选 TCP 四次挥手:连接关闭时交互 FIN/ACK 报文。
问题 9:分库分表以后非分片的 key 如何保证查询效率?
答案:核心是避免 “全库全表扫描”,优化方案按优先级排序:
- 二级索引表:新建 “非分片键 → 分片键” 映射表(如 order_id → user_id),先查映射表拿到分片键,再路由查询;
- 全局索引:用 Elasticsearch 构建全量数据索引,非分片键查询先查 ES 拿到分片键,再查数据库;
- 广播表:非分片键为静态小表(如币种、地区),每个分库存储一份,直接查询;
- 二次查询:先按非分片键范围过滤,再按分片键路由(如按时间查近 7 天,再按 user_id 路由);
- 中间件优化:用 Sharding-JDBC 的
HintManager手动指定分片键,强制路由。
问题 10:SQL 优化
答案:SQL 优化核心围绕 “减少 IO、减少扫描行数”,核心措施:
-
索引优化:
- 建覆盖索引避免回表;
- 联合索引遵循最左前缀原则;
- 禁止在索引列做函数 / 运算(如
DATE(create_time) = '2024-01-01');
-
语句优化:
- 禁用
SELECT *,只查需要字段; - 批量操作(INSERT INTO ... VALUES (),(),());
- 避免 OR/IN,用 EXISTS 替代;
- 分页优化:用主键游标分页(
WHERE id > 1000 LIMIT 10);
- 禁用
-
执行计划:用
EXPLAIN分析,优先优化type=ALL(全表扫描)、Extra=Using filesort/Using temporary; -
数据库配置:调大
innodb_buffer_pool_size(内存 70%),开启查询缓存(读多写少场景)。
问题 11:联合索引怎么走的?
答案:联合索引(如 idx_a_b_c(a,b,c))按 “最左前缀原则” 匹配,索引树按 a→b→c 排序,查询规则:
- 匹配最左前缀:
WHERE a=1/a=1 AND b=2/a=1 AND b=2 AND c=3均走索引; - 不匹配最左前缀:
WHERE b=2/WHERE c=3不走索引;WHERE a=1 AND c=3仅 a 走索引,c 不走; - 范围查询截断:
WHERE a=1 AND b>2 AND c=3,a、b 走索引,c 因范围查询后失效,不走索引; - 排序优化:
ORDER BY a,b,c可利用索引排序,避免Using filesort。
问题 12:Mysql 执行计划(EXPLAIN)怎么看?
答案:
(1)核心字段说明
| 字段 | 含义 | 最优值 / 关键说明 |
|---|---|---|
| id | 查询执行顺序 | 数字越大越先执行,相同则从上到下 |
| type | 访问类型 | const > eq_ref > ref > range > ALL |
| key | 实际使用的索引 | 不为 NULL 表示走索引 |
| rows | 预估扫描行数 | 越少越好 |
| Extra | 额外信息 | Using index(最优)、Using filesort/Using temporary(需优化) |
(2)实战示例(带答案)
SQL:SELECT amount FROM t_order WHERE user_id=10086 AND create_time > '2024-01-01';执行计划:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t_order | range | idx_user_id,idx_ct | idx_user_id | 100 | Using where; Using index condition |
分析答案:
- type=range:走范围索引,优于 ALL 但差于 ref;
- key=idx_user_id:仅使用 user_id 索引,create_time 未走索引;
- Extra=Using index condition:有索引下推,但未覆盖索引(需回表查 amount);
- 优化:创建
idx_user_ct_amount(user_id, create_time, amount)覆盖索引,Extra 变为Using index,无回表。