Java后端面试

面经

2025.03.19 小红书日常一面

  1. IO和NIO的区别
  2. JVM内存模型,堆里面具体有哪些东西
  3. 手写监听者模式/单例模式
  4. ThreadLocal实现原理,并发安全吗,map里面放什么东西,内存泄露如何排查
  5. get请求里面放什么,参数如何区分
  6. 讲一下泛型,泛型类的字节码文件是怎样的。
  7. Spring 注解,RequestBody什么用处
  8. ArrayList如何扩容
  9. 虚拟机栈有多大?一个栈帧有多大?
  10. SQL的UPDATE语句的准确写法

2025.03.20 SAP日常二面

  1. two sum, three sum
  2. 缓存空值有问题,应选择布隆过滤器
  3. 限流
  4. Zookeeper 了解吗(因为我讲 Redisson 的时候提到了)
  5. 如果用户名+密码登录,如何保证数据安全。如何加密?RSA?
  6. 点评项目中,点赞数量太多怎么办。(大 key问题)

2025.03.20 阿里云暑期一面

  1. 前缀树:输入法提示
  2. Redis内部有锁吗
  3. 讲讲SETNX
  4. sorted set的底层实现
  5. RDB,AOF的优缺点
  6. 分布式锁
  7. 登录验证,为什么要把token存在redis里面,还有更好的方法吗?了解Oauth2吗?
  8. ThreadLocal 使用的时候需要注意什么问题?
  9. 是先拿锁,再判断库存;还是先在 lua 脚本里面判断库存,再拿锁
  10. 有对商户列表做缓存吗?如何解决按照不同的条件排序还能做到缓存?

2025.03.21 PayPal上海 full stack 暑期实习一面

  1. 合并排序数组
  2. 数字符串里面一共有多少个 palindrome

2025.03.22 月之暗面搜推实习一面

  1. 协程
  2. 讲讲go reduce这个项目的细节
  3. 手写单例模式,除了双重检验锁,还有什么别的方法可以实现吗?懒汉/饿汉。为什么加 volatile,禁止指令重排序,开辟空间,(实例化,分配给该对象)。
  4. Redis的数据结构,为什么不用string而是用hash,我说少一步序列化操作

2025.03.24 SAP日常二面

  1. 为什么用Kafka?优点;用的是Confluent Kafka吗?
  2. 讲讲对分布式锁的理解,项目代码里面如何实现?我回答MultiLock.

2025.03.25 字节暑期一面

  1. 如果某条 SQL 执行太慢,怎么排查,用过什么工具。联表查询:小表驱动大表。
  2. EXPLAIN 会出现一个表格,表格最后有一个 extra column,请介绍
  3. 方法区里面有什么?
  4. 讲一讲 G1
  5. 为什么需要四次挥手?client 最后为什么要 wait?
  6. 讲一讲建立HTTPS连接的过程
  7. 手撕 48 和 3
  8. MySQL主从复制讲一下
  9. 讲一下Spring的AOP,并且除了动态代理的实现,还有什么实现方法?ASM 字节码操作。

2025.03.26 PayPal暑期二面

  1. 手写快排
  2. 为啥要把用户信息存到ThreadLocal里面去
  3. filter和interceptor的区别
  4. interceptor 和 AOP的区别

2025.03.27 字节暑期二面

  1. 合并 k个有序数组
  2. 讲讲 redo log
  3. 讲讲MySQL锁

2025.03.27 初创团队实习

  1. 有用过AWS实现多个服务之间通信吗?

2025.04.01 淘天暑期一面

  1. 讲SpringMVC如何处理请求
  2. 联合索引最左匹配
  3. 线程池

面试准备

git

git merge:适合团队协作

将两个分支的提交历史合并,生成一个新的合并提交(merge commit),保留原始分支的完整历史(包括分叉和合并的痕迹)。 Alt text

1
2
3
4
git checkout main          # 切换到主分支
git pull origin main       # 确保主分支是最新的
git merge feature-branch   # 将特性分支合并到主分支
git push origin main       # 推送合并后的主分支

如果遇到冲突:

1
2
3
4
git merge feature-branch   # 合并时发生冲突
# 手动解决冲突
git add <冲突文件>         # 标记冲突已解决
git commit                 # 完成合并

git rebase

适合于自己操作, 主要作用是保持线性的project history。

1
2
3
4
git checkout main          # 切换到 main 分支
git pull origin main       # 拉取远程 main 分支的最新代码
git checkout feature-branch
git rebase mains

如果有冲突

1
2
git add <冲突文件>      # 标记冲突已解决
git rebase --continue   # 继续 rebase

到这一步,此时是这样的

1
2
3
A --- B --- E [main] [origin/main]
             \
              C' --- D' [feature-branch]

最后合并到主分支

1
2
git checkout main
git merge feature-branch

执行完这一步之后

1
A --- B --- E --- C' --- D' [main] [feature-branch]

(main 和 feature-branch 现在指向同一个提交 D’)

密码加密

用户输入用户名 → 前端请求服务端获取动态 Salt。

服务端生成动态 Salt → 随机生成(如 16 字节),存入 Redis/Session(5 分钟过期),返回给前端。

前端计算哈希 → 拼接 密码 + 动态 Salt,用 SHA-256/PBKDF2 哈希,得到 clientHash。

发送登录请求 → 提交 username + clientHash + sessionId(HTTPS 传输)。

服务端验证 → 根据 sessionId 取出 dynamicSalt,结合数据库中的 bcrypt 哈希 计算预期哈希,对比 clientHash。

登录成功/失败 → 立即废弃 dynamicSalt,防止重放攻击。

数据库分库、分表

token 登陆验证

Token 自包含性:Token(如 JWT)本身携带了用户的身份信息(如用户ID、权限等),服务端无需在内存或数据库中保存会话状态。

服务端不存储会话:服务端仅需验证 Token 的合法性(如签名、有效期等),无需维护用户的登录状态。

每次请求独立:客户端每次请求需主动携带 Token,服务端通过解析 Token 直接获取用户信息,不依赖之前的请求上下文。

  1. 与有状态(Stateful)的对比 有状态示例:传统的 Session 机制,服务端需在内存或数据库中存储 Session ID 和用户数据的映射关系。

无状态优势:避免了服务端的存储开销,更适合分布式系统,容易横向扩展。

  1. 注意事项 Token 的无效化:无状态方案中,若需强制注销 Token(如用户退出登录),需额外设计机制(如短有效期 + 黑名单或使用 Refresh Token)。

安全性:Token 需通过加密签名(如 JWT 的 HMAC 或 RSA)防止篡改,且应通过 HTTPS 传输。

HTTPS

  1. 客户端发起 HTTPS 请求. 包含:
  • 支持的加密算法(如 AES-256-GCM),支持的加密协议版本(如TLS1.2)

  • 随机数 Client Random。

  1. 服务器响应.
  • 选定的加密算法(如 ECDHE-RSA-AES256-GCM-SHA384)。

  • SSL 证书(含公钥 + 域名信息)。

  • 随机数 Server Random。

  1. 客户端验证证书 检查证书是否:
  • 由可信 CA(如 Let’s Encrypt) 签发。

  • 域名匹配且未过期。

  1. 密钥交换(ECDHE)
  • 客户端生成 Pre-Master Secret,用服务器公钥加密后发送。

  • 双方通过 Client Random + Server Random + Pre-Master Secret 生成会话密钥(Session Key)。

  • 客户端生成 finished 报文,用会话密钥加密生成摘要,发给服务器。

  1. 第四次握手
  • 服务器拿私钥解密Pre-Master Secret,然后生成会话密钥;接着生成 finished 报文,用会话密钥加密,发给客户端。
  1. 对称加密
  • 后续所有数据用 AES-256 对称加密传输,密钥仅本次会话有效。

ThreadLocal不适用于分布式系统

在分布式架构中,用户的请求可能被负载均衡到 不同的服务器节点(不同的 JVM 进程)。

即使用户登录后,在 Server A 的 ThreadLocal 中存储了用户信息,但下一次请求可能被路由到 Server B,导致 ThreadLocal 取不到数据。

自增的时候 Redis 宕机:本地缓存/集群多个写节点/服务降级(直接生成 UUID)

  1. RefreshTokenInterceptor
  2. LogInterceptor

操作系统内核态和用户态的区别、切换时会切换哪些资源。

程序计数器(PC,存放下一条要执行指令的地址)、通用寄存器(存储临时数据)

虚拟内存原理

虚拟内存是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

地址空间划分

操作系统为每个进程分配一个独立的虚拟地址空间,这个空间通常比计算机实际的物理内存大。虚拟地址空间被划分为多个页面,页面大小通常是固定的,常见的有 4KB 或 8KB 等。同样,物理内存也被划分为与虚拟页面大小相同的物理页面,也称为页框。

页表机制

为了实现虚拟地址到物理地址的映射,操作系统使用页表来记录这种映射关系。每个进程都有一个对应的页表,页表中的每一项记录了一个虚拟页面与物理页面的映射关系,包括物理页面的地址、页面的访问权限(如可读、可写、可执行等)以及一些状态位(如页面是否在内存中、是否被修改过等)。

当进程访问一个虚拟地址时,硬件会自动根据虚拟地址中的页号在页表中查找对应的物理页号,然后将虚拟地址中的页内偏移量与物理页号组合起来,形成实际的物理地址,从而实现对内存的访问。

缺页中断

如果进程访问的虚拟页面不在物理内存中(即页表中对应的页面状态位表示该页面不在内存),则会发生缺页中断。此时,操作系统会暂停当前进程的执行,将所需的页面从磁盘中读取到物理内存中,并更新页表中的映射关系和状态位。然后,操作系统会重新执行引发缺页中断的指令,使得进程能够继续访问所需的数据。

页面置换算法

当物理内存已满,而又需要加载新的页面时,操作系统需要选择一个已在内存中的页面将其置换到磁盘上,以便为新页面腾出空间。常见的页面置换算法有先进先出(FIFO)算法、最近最少使用(LRU)算法、最不经常使用(LFU)算法等。

  • FIFO 算法:选择最早进入内存的页面进行置换,它的优点是实现简单,但可能会把一些经常使用的页面置换出去,导致性能下降。
  • LRU 算法:选择最近一段时间内最久未使用的页面进行置换,它基于局部性原理,认为最近使用过的页面在未来一段时间内也可能会被频繁使用,因此具有较好的性能。
  • LFU 算法:选择访问次数最少的页面进行置换,它通过记录页面的访问次数来判断页面的使用频率,但是实现相对复杂,并且可能会因为早期的访问次数过多而保留一些不再使用的页面。

Go协程

Go 的协程(Goroutine)说白了就是 Go 语言里的 “轻量级线程”,它和传统线程最大的区别是超级省资源—— 创建一个协程只需要几 KB 内存(线程要几 MB),而且调度由 Go 运行时(Goroutine 调度器)在用户态完成,不用像线程那样频繁陷入内核,所以能轻松创建上万个协程。

比如我启动 1000 个协程并发处理网络请求,每个协程就像一个独立的任务小兵,Go 会自动帮我把它们分配到 CPU 核心上跑,遇到 I/O 阻塞时还会智能切换,特别适合高并发场景。

将大量的 Goroutine(N)动态映射到少量的 OS 线程(M)上执行,由 Go 运行时(Runtime)负责调度,而非操作系统内核。

GIL

GIL(全局解释器锁)是什么?

GIL(Global Interpreter Lock) 是 Python 解释器(特别是 CPython)中的一个机制,它确保 同一时刻只有一个线程执行 Python 字节码。

为什么存在?

Python 的内存管理(如垃圾回收)不是线程安全的,GIL 可以防止多线程竞争导致的数据混乱。

影响:

多线程在 CPU 密集型任务中无法并行(因为 GIL 会串行化线程执行)。

I/O 密集型任务不受太大影响(因为线程在等待 I/O 时会释放 GIL)。

如何绕过?

用 多进程(multiprocessing) 或 协程(asyncio) 替代多线程。 Alt text