📌 本文整理了 Redis 和 Java 多线程方向的高频面试题,每道题附有对比表格、代码示例、项目业务场景、记忆口诀和标准答法,适合背诵和理解。


目录

  • T1 · Redis 常用的数据类型有哪些?说出每种类型的应用场景

  • T2 · 线程和进程有什么区别?

  • T3 · 线程的创建有哪几种方式?

  • T4 · 创建线程池的几种方式

  • T5 · 多线程中常用的工具类有哪些?

  • T6 · 线程池都有哪些类型?

  • T7 · 线程的状态都有哪些?

  • T8 · synchronized 与 Lock 有啥区别?

  • T9 · start() 和 run() 有啥区别?


T1 · Redis 常用的数据类型有哪些?

【初级】说出每种类型的应用场景,要跟项目的业务结合

一句话总结

Redis 有 5 种基础数据类型 + 3 种高级数据类型,选哪种看数据结构和操作需求。


1. String(字符串)— 最通用的类型

特点:存字符串/数字,支持原子自增

场景

怎么说

热点缓存

"把商品详情、用户信息查出来后用 SET 缓存到 Redis,设置 10 分钟过期,减轻数据库压力"

分布式 Session

"用户登录后把 Session 存到 Redis,解决集群部署时 Session 不共享的问题"

接口限流

"用 INCR 计数 + EXPIRE 设过期时间,控制某个用户 1 分钟内最多请求 100 次"

分布式锁

"用 SET key value NX PX 实现加锁,配合 Lua 脚本保证原子释放,防止超卖"

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,可以只改某个字段

场景

怎么说

购物车

"key 是 cart:用户ID,field 是商品ID,value 是数量。加购用 HSET,改数量用 HINCRBY,比 String 序列化整个对象更灵活"

用户信息缓存

"把用户的名字、积分、会员等级分别存在不同 field,更新积分时只改积分字段,不用读整个对象再写回"

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 可阻塞消费

场景

怎么说

消息队列(轻量)

"发送短信/邮件这种异步任务,生产者 LPUSH 任务,消费者 BRPOP 阻塞消费,不用引入 MQ 中间件"

最近浏览记录

"用 LPUSH 把最新商品加到头部,再用 LTRIM 只保留最近 10 条,实现用户浏览历史功能"

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 做交并差

场景

怎么说

共同好友

"把两个用户的好友列表分别存 Set,SINTER 取交集就是共同好友"

抽奖

"把参与用户都 SADD 进去,SPOP 随机弹出就是中奖人,天然去重不会重复中奖"

黑名单

"IP 黑名单用 Set 存,SISMEMBER 判断是否在黑名单,O(1) 时间复杂度"

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 自动排序

场景

怎么说

排行榜

"游戏积分榜、商品销量榜,用 ZINCRBY 更新分数,ZREVRANGE 0 9 取前 10 名"

延时队列

"订单超时关闭:下单时用 ZADD 把订单加进去,score 设为超时时间戳,定时任务扫描到期任务去关闭"

热搜榜

"每次有人搜索就 ZINCRBY 加分,ZREVRANGE 取 Top10 就是实时热搜"

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. 三种高级数据类型

类型

业务场景

示例

Bitmap(位图)

用户签到(365天只需46字节)

SETBIT sign:uid:2026 156 1

HyperLogLog

海量 UV 统计(12KB,误差0.81%)

PFADD uv:page_home uid_1

Geo(地理位置)

附近的人、外卖配送范围

GEORADIUS drivers 116 39 3 km


选型决策表

需求                   → 推荐类型

───────────────────────────────────

缓存对象/计数/锁 → String

存对象且要局部更新 → Hash

有序队列/时间线 → List

去重/集合运算 → Set

排行榜/延时队列 → Sorted Set

签到/布尔标记 → Bitmap

海量UV统计 → HyperLogLog

地理位置/附近的人 → Geo

面试标准答法

"在我们项目中,String 用来做缓存和分布式锁,Hash 存购物车,ZSet 做商品销量排行榜,Set 做标签系统和共同好友,List 做轻量异步队列。选哪种类型主要看数据结构和操作需求。"


T2 · 线程和进程有什么区别?

【初级】

一句话总结

进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程是"工厂",线程是工厂里的"工人"。

核心区别对比表

对比维度

进程

线程

定义

程序的一次执行实例

进程内的一条执行路径

资源

独立拥有内存、文件句柄等资源

共享所在进程的资源

切换开销

大(要切换内存空间)

小(共享内存,切换快)

创建销毁

慢、开销大

快、开销小

通信方式

复杂(管道、Socket、共享内存)

简单(直接读写共享变量)

安全性

互不影响(一个崩溃不影响其他)

一个线程崩溃可能导致整个进程崩溃

内存结构

线程独有:程序计数器、虚拟机栈、本地方法栈

线程共享:堆内存、方法区(元空间)、文件句柄

结合项目说

  • 进程:"微服务架构中,每个服务(订单、用户、商品)都是独立的进程,互相隔离,一个挂了不影响其他"

  • 线程:"订单服务内部,下单时用多线程异步发短信、更新库存,提高接口响应速度"

面试常问延伸

  • 为什么线程切换更快? 进程切换要切换内存地址空间(换页表),线程只切换寄存器和栈

  • 线程共享带来什么问题? 线程安全问题,需要 synchronized、Lock、volatile 等来保证

  • Java 线程与 OS 线程的关系? HotSpot JVM 中是 1:1 映射

面试标准答法

"进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程之间相互独立,有各自的内存空间;同一进程内的线程共享堆内存和方法区,各自有独立的栈。进程切换开销大但安全性高;线程切换开销小但要注意线程安全问题。在项目中,微服务用进程隔离保证高可用,服务内部用多线程+线程池提升并发处理能力。"


T3 · 线程的创建有哪几种方式?

【初级】

一句话总结

Java 创建线程有 4 种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口(有返回值)、使用线程池。实际项目中推荐线程池。


方式一:继承 Thread 类

java

public class MyThread extends Thread {

@Override

public void run() {

System.out.println("线程执行:" + Thread.currentThread().getName());

}

}

MyThread t = new MyThread();

t.start();

缺点:Java 单继承限制,继承了 Thread 就不能继承其他类 ❌


方式二:实现 Runnable 接口 ✅

java

new Thread(() -> System.out.println("执行任务")).start();

优点:避免单继承限制 ✅


方式三:实现 Callable 接口(有返回值)✅

java

FutureTask<String> futureTask = new FutureTask<>(() -> "任务结果");

new Thread(futureTask).start();

String result = futureTask.get(); // 阻塞获取结果

Runnable vs Callable 对比:

Runnable

Callable

返回值

❌ void

✅ 泛型

抛异常

❌ 不能

✅ 能

方法名

run()

call()


方式四:线程池 ⭐⭐⭐

java

ExecutorService 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(❌ 不推荐)

类型

特点

风险

固定线程数 newFixedThreadPool

线程数固定

队列无界 → 任务堆积 OOM

可缓存线程 newCachedThreadPool

线程动态增减

最大线程无限 → 线程爆炸 OOM

单线程 newSingleThreadExecutor

只有 1 个线程

队列无界 → 任务堆积 OOM

定时任务 newScheduledThreadPool

支持延迟/周期执行

最大线程无限 → 线程爆炸 OOM

阿里开发手册强制规定:不允许使用 Executors 创建线程池!


方式二:手动创建 ThreadPoolExecutor(✅ 推荐)

java

ThreadPoolExecutor 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 拒绝策略

);

线程池执行流程

来了任务 → 核心线程数没满?→ 是 → 创建核心线程执行

→ 否 → 队列没满?→ 是 → 放入队列等待

→ 否 → 最大线程没满?→ 是 → 创建临时线程

→ 否 → 执行拒绝策略

四种拒绝策略

策略

说明

适用场景

中止策略 AbortPolicy(默认)

抛异常

不能丢任务,需感知异常

调用者运行策略 CallerRunsPolicy

提交任务的线程自己执行

不想丢任务,降低提交速度

丢弃策略 DiscardPolicy

直接丢弃

允许丢任务

丢弃最老策略 DiscardOldestPolicy

丢弃队列最老的

允许丢旧任务

面试标准答法

"创建线程池有两种方式:线程池工具类和手动创建线程池执行器。工具类提供了固定线程数、可缓存线程、单线程、定时任务四种,但阿里开发手册明确禁止使用,因为它们要么队列无界、要么最大线程数无限,都会导致内存溢出。项目中都是手动指定 7 个参数创建线程池,用有界队列,拒绝策略一般用调用者运行策略避免任务丢失。"


T5 · 多线程中常用的工具类有哪些?

【中级】

一句话总结

常用工具类有倒计时门闩、循环屏障、条件变量。面试重点考前两个的区别。


(1)倒计时门闩 CountDownLatch

让某一条线程等待其他线程执行完毕后再执行

java

CountDownLatch latch = new CountDownLatch(3);

// 子线程完成后

latch.countDown(); // 计数器 -1

// 主线程

latch.await(); // 阻塞等计数器归0

业务场景:"下单时并行查库存、查用户、查优惠券,主线程等三个都查完再汇总返回,串行 300ms 优化成并行 100ms"


(2)循环屏障 CyclicBarrier

让多条线程都准备就绪后,一起开始执行

java

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("全部到齐"));

// 每个线程

barrier.await(); // 等其他线程到齐

业务场景:"多线程分批导入数据,每批等所有线程处理完才开始下一批,循环屏障可以重复使用"


(3)条件变量 Condition

精确唤醒某条线程

java

Lock lock = new ReentrantLock();

Condition producerCond = lock.newCondition();

Condition consumerCond = lock.newCondition();

producerCond.await(); // 生产者等待

consumerCond.signal(); // 精准唤醒消费者

业务场景:"生产者消费者模型中,队列满了精准唤醒消费者,队列空了精准唤醒生产者"


对比记忆

工具类

核心作用

能否重用

倒计时门闩 CountDownLatch

一等多:一个线程等其他完成

❌ 一次性

循环屏障 CyclicBarrier

多等多:所有线程互相等,一起出发

✅ 可重复

条件变量 Condition

精准唤醒:指定唤醒某个等待线程

✅ 可重复

记忆口诀

门闩:一等多,用完即废

屏障:多等多,可以循环

条件:精准唤醒,配合Lock用


T6 · 线程池都有哪些类型?

【中级】

一句话总结

线程池工具类提供了四种:固定线程数、可缓存、单线程、定时任务。但项目中都用手动创建,因为这四种都有内存溢出风险。


四种类型详解

1. 固定线程数线程池

线程数固定不变,多余任务排队。核心=最大=N,队列无界。

适合:任务量稳定的场景(如批量发报表邮件)

风险:队列无界,任务堆积 → 内存溢出


2. 可缓存线程池

线程数随任务动态增减,空闲 60 秒回收。核心=0,最大=无限。

适合:任务量波动大、执行时间短的场景(如批量发短信)

风险:高并发无限创建线程 → 内存溢出


3. 单线程线程池

只有 1 个线程,所有任务串行执行。队列无界。

适合:需要保证顺序的场景(如日志按序写入)

风险:队列无界,任务堆积 → 内存溢出


4. 定时任务线程池

支持延迟执行和周期性执行。最大线程=无限。

适合:定时清理、周期同步数据

风险:最大线程无限 → 内存溢出


面试标准答法

"线程池工具类提供了四种:固定线程数、可缓存线程、单线程、定时任务线程池。固定和单线程用的是无界队列,可缓存和定时任务最大线程数无限,四种都有内存溢出风险。阿里开发手册明确规定不允许用这种方式,项目中都是手动指定参数创建线程池,用有界队列和合理的最大线程数来规避风险。"


T7 · 线程的状态都有哪些?

【中级】

一句话总结

Java 线程有 6 种状态:新建、可运行、阻塞、无限等待、超时等待、终止。

6 种状态

状态

中文

说明

NEW

新建

创建了线程对象,还没调 start()

RUNNABLE

可运行

调了 start(),包含"就绪"和"运行中"

BLOCKED

阻塞

等待获取 synchronized 锁

WAITING

无限等待

调了 wait()、join(),需要被唤醒

TIMED_WAITING

超时等待

调了 sleep(n)、wait(n),时间到自动醒

TERMINATED

终止

执行完毕或异常退出,不可逆

状态流转

NEW → start() → RUNNABLE

RUNNABLE → synchronized抢锁失败 → BLOCKED → 抢到锁 → RUNNABLE

RUNNABLE → wait()/join() → WAITING → notify() → RUNNABLE

RUNNABLE → sleep(n)/wait(n) → TIMED_WAITING → 时间到 → RUNNABLE

RUNNABLE → 执行完毕/异常 → TERMINATED

三种"等待"状态的区别

BLOCKED

WAITING

TIMED_WAITING

原因

抢锁失败

主动等待

主动等待+超时

触发

synchronized

wait() / join()

sleep(n) / wait(n)

恢复

抢到锁

notify() 唤醒

时间到自动恢复

关于 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 是接口,手动加锁释放锁,功能更强大灵活。

核心区别对比表

对比维度

synchronized

Lock

来源

Java 关键字

java.util.concurrent 包接口

加锁/释放

自动

手动(必须 finally 里 unlock)

获取锁失败

一直等,不可中断

可中断等待

超时获取

✅ tryLock(time)

公平锁

❌ 非公平

✅ 可选公平/非公平

精准唤醒

❌ notify 随机唤醒

✅ Condition 精准唤醒

Lock 独有的三大能力

① 可中断等待

java

lock.lockInterruptibly(); // 等待过程中可以被中断

防止线程因为等锁而永久僵死。

② 超时获取锁

java

if (lock.tryLock(3, TimeUnit.SECONDS)) {

try { /* 业务 */ } finally { lock.unlock(); }

} else {

// 降级处理

}

避免接口因等锁超时。

③ 精准唤醒

java

Condition 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() 只是普通方法调用,不会创建新线程。

核心区别

start()

run()

创建新线程

✅ 创建

❌ 不创建

执行方式

异步

同步(普通方法调用)

能调几次

只能一次,第二次抛异常

可以多次

谁在执行

新的子线程

调用它的线程(通常是主线程)

代码对比

java

// start() — 新线程执行

t.start();

// 输出:执行线程:Thread-0 ← 新线程

// run() — 还是主线程执行

t.run();

// 输出:执行线程:main ← 还是主线程!

start() 只能调一次

java

t.start(); // ✅ 第一次正常

t.start(); // ❌ 抛 IllegalThreadStateException

线程状态从 NEW → RUNNABLE → TERMINATED,不能回头。

start() 底层流程

start() → 检查状态是否 NEW → 调用 JVM 本地方法 start0()

→ JVM 通知操作系统创建内核线程 → 新线程执行 run()

面试标准答法

"start() 会创建一个新线程,由新线程异步执行 run() 里的代码;直接调 run() 不会创建新线程,只是在当前线程同步执行,和调普通方法没有区别。另外 start() 只能调一次,第二次会抛出非法线程状态异常。"

记忆口诀

start() → 创建新线程 → 异步 → 只能调一次

run() → 普通方法 → 同步 → 可以调多次

直接调 run() = 没有多线程,白写了


📋 全文速查表

题号

题目

难度

核心考点

T1

Redis 数据类型与业务场景

初级

5+3 种类型,结合业务说场景

T2

线程和进程的区别

初级

资源分配 vs CPU 调度

T3

线程的创建方式

初级

4 种方式,推荐线程池

T4

创建线程池的方式

中级

工具类 vs 手动创建,7 个参数

T5

多线程常用工具类

中级

门闩、屏障、条件变量的区别

T6

线程池的类型

中级

4 种类型及 OOM 风险

T7

线程的状态

中级

6 种状态及流转

T8

synchronized vs Lock

中级

Lock 三大独有能力

T9

start() vs run()

初级

是否创建新线程


✍️ 持续更新中... 欢迎收藏 ⭐