Java 多线程高频八股文:从线程创建到线程池,一篇搞懂常见面试题
前言
Java 多线程是 Java 后端面试中非常高频的一块内容。
平时写业务代码时,可能不会天天手写线程,但在面试中,经常会被问到:
- 线程和进程有什么区别?
- 创建线程有哪几种方式?
- sleep() 和 wait() 有什么区别?
- synchronized 和 volatile 有什么区别?
- CAS 是什么?
- ThreadLocal 是什么?
- 线程池七大参数是什么?
- 线程池执行流程是什么?
- 死锁怎么产生?怎么解决?
这篇文章整理一份 Java 多线程高频八股文,适合初学者和准备面试的同学快速复习。
1. 什么是进程和线程?
进程
进程是操作系统分配资源的基本单位。
一个正在运行的程序,就可以看作一个进程。
比如:
运行中的 IDEA
运行中的 MySQL
运行中的浏览器
线程
线程是 CPU 调度的基本单位。
一个进程中可以包含多个线程。
比如浏览器进程中可能有:
页面渲染线程
网络请求线程
JS 执行线程
简单理解:
进程是资源分配单位,线程是程序执行单位。
2. 进程和线程有什么区别?
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源 | 拥有独立资源 | 共享进程资源 |
| 开销 | 创建和切换开销大 | 创建和切换开销小 |
| 通信 | 通信比较复杂 | 通信比较方便 |
| 稳定性 | 一个进程崩溃通常不影响其他进程 | 一个线程异常可能影响整个进程 |
面试时可以这样回答:
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程可以包含多个线程,线程之间共享进程的内存资源,所以线程通信更方便,但也更容易产生线程安全问题。
3. 创建线程有哪几种方式?
Java 中常见创建线程的方式有四种:
继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
使用:
new MyThread().start();
实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
使用:
new Thread(new MyRunnable()).start();
实现 Callable 接口
class MyCallable implements Callable<String> {
@Override
public String call() {
return "执行结果";
}
}
Callable 可以有返回值,也可以抛出异常。
使用线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
System.out.println("线程池执行任务");
});
实际开发中更推荐使用线程池,而不是频繁手动创建线程。
4. Runnable 和 Callable 有什么区别?
| 对比项 | Runnable | Callable |
|---|---|---|
| 返回值 | 没有返回值 | 有返回值 |
| 方法 | run() | call() |
| 异常 | 不能直接抛出受检异常 | 可以抛出异常 |
| 配合结果 | 不返回结果 | 通常配合 Future 获取结果 |
Runnable 示例:
Runnable runnable = () -> System.out.println("执行任务");
Callable 示例:
Callable<String> callable = () -> "执行结果";
简单总结:
Runnable 适合不需要返回结果的任务,Callable 适合需要返回结果的任务。
5. start() 和 run() 有什么区别?
start() 是启动线程。
调用 start() 后,JVM 会创建一个新的线程,并让这个线程去执行 run() 方法。
new Thread(() -> {
System.out.println("执行任务");
}).start();
run() 只是一个普通方法。
如果直接调用 run(),不会创建新线程,而是在当前线程中执行。
thread.run();
面试回答:
start() 会真正启动一个新线程,run() 只是普通方法调用,不会开启新线程。
6. 线程有哪些状态?
Java 线程常见状态有:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
NEW
线程刚创建,还没有调用 start()。
RUNNABLE
线程已经启动,正在运行或等待 CPU 调度。
BLOCKED
线程等待获取锁。
WAITING
线程无限期等待,需要其他线程唤醒。
TIMED_WAITING
线程限时等待,比如 sleep()。
TERMINATED
线程执行结束。
7. sleep() 和 wait() 有什么区别?
| 对比项 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 是否释放锁 | 不释放锁 | 释放锁 |
| 使用位置 | 任意位置 | 必须在同步代码块或同步方法中 |
| 唤醒方式 | 时间到了自动恢复 | notify 或 notifyAll 唤醒 |
示例:
Thread.sleep(1000);
wait() 示例:
synchronized (lock) {
lock.wait();
}
面试重点:
sleep 不会释放锁,wait 会释放锁。
8. notify() 和 notifyAll() 有什么区别?
notify() 会随机唤醒一个等待该对象锁的线程。
lock.notify();
notifyAll() 会唤醒所有等待该对象锁的线程。
lock.notifyAll();
需要注意:
被唤醒的线程不会立刻执行,而是要重新竞争锁,拿到锁后才能继续执行。
实际开发中,如果不确定唤醒哪个线程更合适,通常使用 notifyAll() 更稳。
9. 什么是线程安全问题?
线程安全问题指的是:
多个线程同时操作共享数据,导致结果不符合预期。
例如:
count++;
这行代码看起来只有一步,实际上可能包含:
读取 count
count 加 1
写回 count
如果多个线程同时执行,就可能出现数据覆盖,导致结果错误。
解决线程安全问题的常见方式有:
synchronized
Lock
volatile
Atomic 原子类
ThreadLocal
10. synchronized 是什么?
synchronized 是 Java 提供的关键字,用来保证线程安全。
它可以修饰:
实例方法
public synchronized void method() {
}
锁的是当前对象 this。
静态方法
public static synchronized void method() {
}
锁的是当前类的 Class 对象。
代码块
synchronized (lock) {
}
锁的是括号中的对象。
synchronized 可以保证:
- 原子性。
- 可见性。
- 有序性。
11. synchronized 的底层原理是什么?
synchronized 底层主要依赖对象监视器 Monitor。
每个 Java 对象都可以作为锁对象。
当线程进入同步代码块时,需要先获取对象的 Monitor。
如果获取成功,就可以执行同步代码。
如果获取失败,就会进入阻塞状态,等待锁释放。
简单理解:
synchronized 是通过对象锁和 Monitor 机制实现线程同步的。
12. synchronized 锁升级是什么?
在 JDK 6 之后,synchronized 做了很多优化。
锁状态大致包括:
无锁 偏向锁 轻量级锁 重量级锁
锁升级过程一般是:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁
适合只有一个线程访问同步代码的场景。
轻量级锁
适合多个线程交替访问,但竞争不激烈的场景。
重量级锁
适合线程竞争激烈的场景,会涉及线程阻塞和唤醒,开销较大。
13. volatile 是什么?
volatile 是 Java 中的关键字,主要作用有两个:
- 保证变量的可见性。
- 禁止指令重排序。
示例:
private volatile boolean flag = true;
当一个线程修改了 flag,其他线程可以立即看到最新值。
但是需要注意:
volatile 不能保证复合操作的原子性。
例如:
count++;
即使 count 使用了 volatile 修饰,也不能保证线程安全。
14. volatile 和 synchronized 有什么区别?
| 对比项 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 一定程度保证 | 保证 |
| 阻塞 | 不会阻塞线程 | 可能阻塞线程 |
| 使用场景 | 状态标记 | 复杂同步操作 |
简单总结:
volatile 更轻量,适合状态标记;synchronized 更强大,适合需要保证原子性的代码块。
15. 什么是 CAS?
CAS 全称是 Compare And Swap,中文叫比较并交换。
它是一种无锁的原子操作。
CAS 包含三个值:
内存值 V 预期值 A 新值 B
执行逻辑是:
如果 V == A,就把 V 改成 B 如果 V != A,说明数据被别人改过,操作失败
Java 中很多原子类都使用了 CAS,例如:
AtomicInteger
AtomicLong
AtomicReference
16. CAS 有什么问题?
CAS 虽然效率高,但也有一些问题:
ABA 问题
一个值从 A 变成 B,又变回 A。
CAS 只判断值是否相等,无法知道中间是否被修改过。
解决方式:
AtomicStampedReference
通过版本号解决 ABA 问题。
自旋开销大
CAS 失败后可能会不断重试,如果竞争激烈,会消耗 CPU。
只能保证单个变量的原子操作
如果要保证多个变量的一致性,CAS 处理起来比较复杂。
17. AtomicInteger 为什么线程安全?
AtomicInteger 底层主要使用 CAS 实现线程安全。
例如:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
多个线程同时执行自增时,AtomicInteger 会不断比较旧值并尝试更新。
如果更新失败,就重新获取最新值再尝试。
所以它可以在不加锁的情况下保证自增操作的原子性。
18. 什么是 ThreadLocal?
ThreadLocal 可以为每个线程保存一份独立的变量副本。
不同线程之间互不影响。
示例:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("user");
String value = threadLocal.get();
threadLocal.remove();
常见使用场景:
- 保存当前登录用户信息。
- 保存数据库连接。
- 保存请求上下文。
- 避免参数层层传递。
简单理解:
ThreadLocal 是线程级别的变量隔离工具。
19. ThreadLocal 底层原理是什么?
每个线程对象内部都有一个 ThreadLocalMap。
当调用:
threadLocal.set(value);
实际上是把数据存到当前线程的 ThreadLocalMap 中。
其中:
key 是 ThreadLocal 对象 value 是存储的值
不同线程有不同的 ThreadLocalMap,所以数据互不影响。
20. ThreadLocal 为什么可能内存泄漏?
ThreadLocalMap 中的 key 是弱引用,value 是强引用。
如果 ThreadLocal 对象被回收,key 会变成 null。
但是 value 还被 ThreadLocalMap 强引用着,可能无法释放。
尤其在线程池中,线程会被复用,如果不清理 ThreadLocal,value 可能长期存在。
所以使用完 ThreadLocal 后,建议调用:
threadLocal.remove();
21. 什么是线程池?
线程池就是提前创建并管理一批线程。
当有任务提交时,不需要每次都创建新线程,而是复用已有线程执行任务。
线程池的好处:
- 减少线程创建和销毁开销。
- 提高响应速度。
- 统一管理线程。
- 防止线程无限创建导致系统资源耗尽。
实际开发中,推荐使用线程池管理异步任务。
22. 为什么不建议使用 Executors 创建线程池?
例如:
Executors.newFixedThreadPool(10);
Executors.newCachedThreadPool();
虽然写法简单,但不太推荐。
原因是:
- newFixedThreadPool 使用的队列可能非常大,容易造成 OOM。
- newCachedThreadPool 最大线程数可能非常大,容易创建过多线程。
- 参数不够明确,不利于排查问题。
实际项目中更推荐手动创建 ThreadPoolExecutor。
23. 线程池七大参数是什么?
ThreadPoolExecutor 有七个核心参数:
new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
分别是:
corePoolSize
核心线程数。
maximumPoolSize
最大线程数。
keepAliveTime
非核心线程空闲存活时间。
unit
时间单位。
workQueue
任务队列。
threadFactory
线程工厂,用来创建线程。
handler
拒绝策略。
这七个参数是线程池面试必背内容。
24. 线程池执行流程是什么?
线程池执行任务的大致流程:
- 如果当前线程数小于核心线程数,创建核心线程执行任务。
- 如果核心线程已满,把任务放入任务队列。
- 如果任务队列已满,并且当前线程数小于最大线程数,创建非核心线程执行任务。
- 如果最大线程数也满了,就执行拒绝策略。
可以简单记成:
核心线程 -> 任务队列 -> 最大线程 -> 拒绝策略
25. 线程池有哪些拒绝策略?
常见拒绝策略有四种:
AbortPolicy
默认策略,直接抛出异常。
CallerRunsPolicy
由提交任务的线程自己执行任务。
DiscardPolicy
直接丢弃任务,不抛异常。
DiscardOldestPolicy
丢弃队列中最早的任务,然后尝试提交当前任务。
实际开发中,拒绝策略要根据业务场景选择,不能随便丢任务。
26. execute() 和 submit() 有什么区别?
| 对比项 | execute() | submit() |
|---|---|---|
| 所属接口 | Executor | ExecutorService |
| 返回值 | 没有返回值 | 返回 Future |
| 异常表现 | 异常直接抛出 | 异常封装在 Future 中 |
| 任务类型 | Runnable | Runnable 或 Callable |
如果不需要返回结果,可以使用:
execute()
如果需要返回结果,可以使用:
submit()
27. 什么是死锁?
死锁指的是多个线程互相等待对方释放资源,导致程序一直无法继续执行。
例如:
线程 A 拿着锁 1,等待锁 2 线程 B 拿着锁 2,等待锁 1
这样两个线程就会一直等待下去。
28. 死锁产生的条件有哪些?
死锁产生通常需要满足四个条件:
- 互斥条件。
- 请求并保持条件。
- 不可剥夺条件。
- 循环等待条件。
只要破坏其中一个条件,就可以避免死锁。
常见解决方式:
- 固定加锁顺序。
- 避免嵌套锁。
- 使用超时锁。
- 减小锁粒度。
- 使用工具排查死锁。
29. 如何排查死锁?
可以使用 JDK 自带工具:
jps jstack
先查看 Java 进程:
jps
再查看线程堆栈:
jstack 进程ID
如果存在死锁,线程堆栈中通常会有相关提示。
也可以使用:
VisualVM Arthas JConsole
这些工具辅助排查线程问题。
30. Java 多线程面试怎么回答更稳?
回答多线程问题时,不要只背一个概念。
建议按照:
是什么 + 为什么 + 怎么用 + 注意点
比如问:“volatile 是什么?”
可以这样回答:
volatile 是 Java 中的关键字,主要作用是保证变量的可见性和禁止指令重排序。一个线程修改 volatile 变量后,其他线程可以及时看到最新值。但 volatile 不能保证复合操作的原子性,比如 count++ 仍然不是线程安全的。如果需要保证原子性,可以使用 synchronized、Lock 或 Atomic 原子类。
再比如问:“线程池执行流程是什么?”
可以这样回答:
线程池收到任务后,会先判断当前线程数是否小于核心线程数,如果小于就创建核心线程执行任务;如果核心线程已满,就把任务放入阻塞队列;如果队列也满了,并且线程数还没达到最大线程数,就创建非核心线程;如果最大线程数也满了,就执行拒绝策略。
总结
Java 多线程是后端面试中的重点内容,常见考点主要包括:
线程和进程 线程创建方式 线程生命周期 sleep 和 wait synchronized volatile CAS Atomic 原子类 ThreadLocal 线程池 死锁
其中最重要的一定是:
- 线程和进程的区别。
- 创建线程的方式。
- sleep 和 wait 的区别。
- synchronized 的使用和原理。
- volatile 的作用。
- CAS 的原理和问题。
- ThreadLocal 的原理和内存泄漏。
- 线程池七大参数和执行流程。
- 死锁产生和解决。
如果是初学者,建议先把这些问题理解清楚,再结合代码练习。面试时不用一上来就讲源码细节,但一定要能把核心概念、使用场景和注意点说清楚。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)