Java 后端高频面试题合集 · Redis + 多线程篇
📌 本文整理了 Redis 和 Java 多线程方向的高频面试题,每道题附有对比表格、代码示例、项目业务场景、记忆口诀和标准答法,适合背诵和理解。
目录
T1 · Redis 常用的数据类型有哪些?说出每种类型的应用场景
T2 · 线程和进程有什么区别?
T3 · 线程的创建有哪几种方式?
T4 · 创建线程池的几种方式
T5 · 多线程中常用的工具类有哪些?
T6 · 线程池都有哪些类型?
T7 · 线程的状态都有哪些?
T8 · synchronized 与 Lock 有啥区别?
T9 · start() 和 run() 有啥区别?
T1 · Redis 常用的数据类型有哪些?
【初级】说出每种类型的应用场景,要跟项目的业务结合
一句话总结
Redis 有 5 种基础数据类型 + 3 种高级数据类型,选哪种看数据结构和操作需求。
1. String(字符串)— 最通用的类型
特点:存字符串/数字,支持原子自增
bash# 商品缓存,10分钟过期
SET product:1001 '{"name":"iPhone","price":9999}' EX 600
# 接口限流
INCR user:rate:uid_123
EXPIRE user:rate:uid_123 60
2. Hash(哈希)— 存对象,可单字段更新
特点:一个 key 对应多个 field-value,可以只改某个字段
bash# 购物车
HSET cart:123 product_456 2
HINCRBY cart:123 product_456 1 # 数量+1
# 用户信息
HSET user:1001 name "张三" age 28 vip_level 3
HGET user:1001 vip_level # 单独获取 VIP 等级
3. List(列表)— 有序列表,天然队列
特点:双向链表,支持头尾操作,BRPOP 可阻塞消费
bash# 最近浏览记录
LPUSH browse_history:uid_123 product_456
LTRIM browse_history:uid_123 0 9
# 简单消息队列
LPUSH task_queue '{"type":"email","to":"user@xxx.com"}'
BRPOP task_queue 0 # 消费者阻塞等待
4. Set(集合)— 无序不重复,支持集合运算
特点:自动去重,SINTER/SUNION/SDIFF 做交并差
bash# 共同好友
SADD friends:uid_A uid_B uid_C uid_D
SADD friends:uid_B uid_A uid_C uid_E
SINTER friends:uid_A friends:uid_B # 共同好友: uid_C
# 抽奖
SADD lottery uid_1 uid_2 uid_3 uid_4 uid_5
SPOP lottery 3 # 随机抽3人
5. Sorted Set / ZSet(有序集合)— 带分数排序
特点:每个元素有 score,按 score 自动排序
bash# 排行榜
ZADD game_rank 8800 "PlayerA"
ZINCRBY game_rank 200 "PlayerA"
ZREVRANGE game_rank 0 9 WITHSCORES # 前10名
# 延时队列
ZADD delay_queue 1748922000 "close_order:ORDER_001"
6. 三种高级数据类型
选型决策表
需求 → 推荐类型───────────────────────────────────
缓存对象/计数/锁 → String
存对象且要局部更新 → Hash
有序队列/时间线 → List
去重/集合运算 → Set
排行榜/延时队列 → Sorted Set
签到/布尔标记 → Bitmap
海量UV统计 → HyperLogLog
地理位置/附近的人 → Geo
面试标准答法
"在我们项目中,String 用来做缓存和分布式锁,Hash 存购物车,ZSet 做商品销量排行榜,Set 做标签系统和共同好友,List 做轻量异步队列。选哪种类型主要看数据结构和操作需求。"
T2 · 线程和进程有什么区别?
【初级】
一句话总结
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程是"工厂",线程是工厂里的"工人"。
核心区别对比表
内存结构
线程独有:程序计数器、虚拟机栈、本地方法栈
线程共享:堆内存、方法区(元空间)、文件句柄
结合项目说
进程:"微服务架构中,每个服务(订单、用户、商品)都是独立的进程,互相隔离,一个挂了不影响其他"
线程:"订单服务内部,下单时用多线程异步发短信、更新库存,提高接口响应速度"
面试常问延伸
为什么线程切换更快? 进程切换要切换内存地址空间(换页表),线程只切换寄存器和栈
线程共享带来什么问题? 线程安全问题,需要 synchronized、Lock、volatile 等来保证
Java 线程与 OS 线程的关系? HotSpot JVM 中是 1:1 映射
面试标准答法
"进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程之间相互独立,有各自的内存空间;同一进程内的线程共享堆内存和方法区,各自有独立的栈。进程切换开销大但安全性高;线程切换开销小但要注意线程安全问题。在项目中,微服务用进程隔离保证高可用,服务内部用多线程+线程池提升并发处理能力。"
T3 · 线程的创建有哪几种方式?
【初级】
一句话总结
Java 创建线程有 4 种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口(有返回值)、使用线程池。实际项目中推荐线程池。
方式一:继承 Thread 类
javapublic class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start();
缺点:Java 单继承限制,继承了 Thread 就不能继承其他类 ❌
方式二:实现 Runnable 接口 ✅
javanew Thread(() -> System.out.println("执行任务")).start();
优点:避免单继承限制 ✅
方式三:实现 Callable 接口(有返回值)✅
javaFutureTask<String> futureTask = new FutureTask<>(() -> "任务结果");
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞获取结果
Runnable vs Callable 对比:
方式四:线程池 ⭐⭐⭐
javaExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(() -> System.out.println("执行任务"));
Future<String> future = executor.submit(() -> "任务结果");
executor.shutdown();
为什么必须用线程池?阿里开发手册强制规定,不允许手动 new Thread,线程池可以复用线程、控制最大并发数、防止 OOM。
面试标准答法
"Java 创建线程有 4 种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。Runnable 和 Callable 的区别是 Callable 有返回值且能抛异常。实际项目中都是用线程池来管理线程,手动配置 ThreadPoolExecutor 的核心参数,避免无限制创建线程导致 OOM,这也是阿里开发手册的强制规范。"
T4 · 创建线程池的几种方式
【中级】
一句话总结
创建线程池有两种方式:用线程池工具类快捷创建(不推荐)、手动创建线程池执行器(推荐)。面试官问这题,99% 是想听你说出为什么不能用工具类创建。
方式一:线程池工具类 Executors(❌ 不推荐)
阿里开发手册强制规定:不允许使用 Executors 创建线程池!
方式二:手动创建 ThreadPoolExecutor(✅ 推荐)
javaThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // ① corePoolSize 核心线程数(正式工)
10, // ② maximumPoolSize 最大线程数(正式工+临时工)
60L, TimeUnit.SECONDS, // ③④ keepAliveTime 临时工空闲存活时间
new LinkedBlockingQueue<>(200), // ⑤ workQueue 有界队列(候客区)
new ThreadFactoryBuilder() // ⑥ threadFactory 线程工厂(可命名)
.setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // ⑦ handler 拒绝策略
);
线程池执行流程
来了任务 → 核心线程数没满?→ 是 → 创建核心线程执行→ 否 → 队列没满?→ 是 → 放入队列等待
→ 否 → 最大线程没满?→ 是 → 创建临时线程
→ 否 → 执行拒绝策略
四种拒绝策略
面试标准答法
"创建线程池有两种方式:线程池工具类和手动创建线程池执行器。工具类提供了固定线程数、可缓存线程、单线程、定时任务四种,但阿里开发手册明确禁止使用,因为它们要么队列无界、要么最大线程数无限,都会导致内存溢出。项目中都是手动指定 7 个参数创建线程池,用有界队列,拒绝策略一般用调用者运行策略避免任务丢失。"
T5 · 多线程中常用的工具类有哪些?
【中级】
一句话总结
常用工具类有倒计时门闩、循环屏障、条件变量。面试重点考前两个的区别。
(1)倒计时门闩 CountDownLatch
让某一条线程等待其他线程执行完毕后再执行
javaCountDownLatch latch = new CountDownLatch(3);
// 子线程完成后
latch.countDown(); // 计数器 -1
// 主线程
latch.await(); // 阻塞等计数器归0
业务场景:"下单时并行查库存、查用户、查优惠券,主线程等三个都查完再汇总返回,串行 300ms 优化成并行 100ms"
(2)循环屏障 CyclicBarrier
让多条线程都准备就绪后,一起开始执行
javaCyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("全部到齐"));
// 每个线程
barrier.await(); // 等其他线程到齐
业务场景:"多线程分批导入数据,每批等所有线程处理完才开始下一批,循环屏障可以重复使用"
(3)条件变量 Condition
精确唤醒某条线程
javaLock lock = new ReentrantLock();
Condition producerCond = lock.newCondition();
Condition consumerCond = lock.newCondition();
producerCond.await(); // 生产者等待
consumerCond.signal(); // 精准唤醒消费者
业务场景:"生产者消费者模型中,队列满了精准唤醒消费者,队列空了精准唤醒生产者"
对比记忆
记忆口诀
门闩:一等多,用完即废屏障:多等多,可以循环
条件:精准唤醒,配合Lock用
T6 · 线程池都有哪些类型?
【中级】
一句话总结
线程池工具类提供了四种:固定线程数、可缓存、单线程、定时任务。但项目中都用手动创建,因为这四种都有内存溢出风险。
四种类型详解
1. 固定线程数线程池
线程数固定不变,多余任务排队。核心=最大=N,队列无界。
适合:任务量稳定的场景(如批量发报表邮件)
风险:队列无界,任务堆积 → 内存溢出
2. 可缓存线程池
线程数随任务动态增减,空闲 60 秒回收。核心=0,最大=无限。
适合:任务量波动大、执行时间短的场景(如批量发短信)
风险:高并发无限创建线程 → 内存溢出
3. 单线程线程池
只有 1 个线程,所有任务串行执行。队列无界。
适合:需要保证顺序的场景(如日志按序写入)
风险:队列无界,任务堆积 → 内存溢出
4. 定时任务线程池
支持延迟执行和周期性执行。最大线程=无限。
适合:定时清理、周期同步数据
风险:最大线程无限 → 内存溢出
面试标准答法
"线程池工具类提供了四种:固定线程数、可缓存线程、单线程、定时任务线程池。固定和单线程用的是无界队列,可缓存和定时任务最大线程数无限,四种都有内存溢出风险。阿里开发手册明确规定不允许用这种方式,项目中都是手动指定参数创建线程池,用有界队列和合理的最大线程数来规避风险。"
T7 · 线程的状态都有哪些?
【中级】
一句话总结
Java 线程有 6 种状态:新建、可运行、阻塞、无限等待、超时等待、终止。
6 种状态
状态流转
NEW → start() → RUNNABLERUNNABLE → synchronized抢锁失败 → BLOCKED → 抢到锁 → RUNNABLE
RUNNABLE → wait()/join() → WAITING → notify() → RUNNABLE
RUNNABLE → sleep(n)/wait(n) → TIMED_WAITING → 时间到 → RUNNABLE
RUNNABLE → 执行完毕/异常 → TERMINATED
三种"等待"状态的区别
关于 RUNNABLE
Java 的 RUNNABLE 包含操作系统的"就绪"和"运行中"两个状态。Java 不区分,由操作系统的 CPU 调度器来区分。Java 只关心"有没有资格被 CPU 执行"。
sleep 和 wait 的区别(常考)
sleep是 Thread 的方法,不释放锁,时间到自动醒wait是 Object 的方法,释放锁,需要 notify() 唤醒
面试标准答法
"Java 线程有 6 种状态:新建、可运行、阻塞、无限等待、超时等待、终止。新建是创建了线程对象但没调 start();调 start() 后进入可运行;遇到 synchronized 抢不到锁进入阻塞;调 wait()/join() 进入无限等待,需要 notify() 唤醒;调 sleep() 进入超时等待,时间到自动恢复;执行完毕进入终止状态,不可逆。常考的区别是 sleep 不释放锁,wait 会释放锁。"
T8 · synchronized 与 Lock 有啥区别?
【中级】
一句话总结
synchronized 是 Java 关键字,自动加锁释放锁,简单但功能有限;Lock 是接口,手动加锁释放锁,功能更强大灵活。
核心区别对比表
Lock 独有的三大能力
① 可中断等待
javalock.lockInterruptibly(); // 等待过程中可以被中断
防止线程因为等锁而永久僵死。
② 超时获取锁
javaif (lock.tryLock(3, TimeUnit.SECONDS)) {
try { /* 业务 */ } finally { lock.unlock(); }
} else {
// 降级处理
}
避免接口因等锁超时。
③ 精准唤醒
javaCondition producerCond = lock.newCondition();
Condition consumerCond = lock.newCondition();
producerCond.await(); // 生产者等
consumerCond.signal(); // 精准唤醒消费者
结合项目说
"秒杀场景中用 Lock 的 tryLock 超时机制,高并发时设置最多等 500 毫秒,抢不到直接返回'系统繁忙',避免线程堆积拖垮服务器。这是 synchronized 做不到的。"
面试标准答法
"synchronized 和 Lock 的主要区别:synchronized 是关键字,自动加锁释放锁;Lock 是接口,需要手动在 finally 里释放。功能上 Lock 更强大,有三点 synchronized 做不到:一是可中断等待;二是超时获取锁,用 tryLock 设超时时间;三是精准唤醒,通过 Condition 唤醒指定线程。简单场景用 synchronized,高并发复杂场景用 Lock。"
记忆口诀
synchronized:关键字、自动释放、简单够用Lock:接口、手动释放、三大能力:可中断、超时、精准唤醒
T9 · start() 和 run() 有啥区别?
【初级】
一句话总结
start() 会创建新线程去执行 run() 里的代码;直接调 run() 只是普通方法调用,不会创建新线程。
核心区别
代码对比
java// start() — 新线程执行
t.start();
// 输出:执行线程:Thread-0 ← 新线程
// run() — 还是主线程执行
t.run();
// 输出:执行线程:main ← 还是主线程!
start() 只能调一次
javat.start(); // ✅ 第一次正常
t.start(); // ❌ 抛 IllegalThreadStateException
线程状态从 NEW → RUNNABLE → TERMINATED,不能回头。
start() 底层流程
start() → 检查状态是否 NEW → 调用 JVM 本地方法 start0()→ JVM 通知操作系统创建内核线程 → 新线程执行 run()
面试标准答法
"start() 会创建一个新线程,由新线程异步执行 run() 里的代码;直接调 run() 不会创建新线程,只是在当前线程同步执行,和调普通方法没有区别。另外 start() 只能调一次,第二次会抛出非法线程状态异常。"
记忆口诀
start() → 创建新线程 → 异步 → 只能调一次run() → 普通方法 → 同步 → 可以调多次
直接调 run() = 没有多线程,白写了
📋 全文速查表
✍️ 持续更新中... 欢迎收藏 ⭐