网站简介
本页面专为准备技术面试的开发者打造,聚焦多线程这一核心并发编程领域,系统梳理常见考点、原理细节与实战陷阱。内容覆盖Java、C++、Python等主流语言的并发模型,结合真实企业面试题与代码实践,帮助您构建完整的并发知识体系。
我们坚持“问题驱动+原理深挖+避坑指南”的编写逻辑,所有内容均基于一线开发者反馈与面试高频趋势整理而成,力求实现信息增益最大化。无论您是初学者还是资深工程师,都能在这里找到所需知识点,实现从“知道怎么做”到“理解为什么”的跃升。
页面采用响应式设计,适配桌面端与移动端;所有导航均为锚点跳转,无实际二级页面跳转,确保加载速度与用户体验。备案号:蜀ICP备2026017620号-5,©2026考ACCA网 版权所有。
核心价值
- 深度覆盖:从线程生命周期到JMM模型,从synchronized原理到线程池调优,覆盖99%面试考点
- 实战导向:每类知识点配套典型代码示例,包含常见错误与修复方案
- 结构化呈现:采用选项卡、时间轴、FAQ等多形式组织内容,便于快速定位与复习
- 持续更新:根据JDK演进(如Java 21虚拟线程)、并发库变化动态补充新内容
温馨提示:并发编程是工程能力的“分水岭”——掌握者能写出高吞吐、低延迟的系统,而忽视者易陷入死锁、数据不一致等隐性Bug。建议结合本文内容,动手实现至少3个线程协作模型(如生产者-消费者、线程安全单例),效果更佳。
一、多线程基础概念
什么是线程?线程与进程的区别?
线程是操作系统调度的最小单位,是进程内的实际执行单元。一个进程可包含多个线程,它们共享进程的内存空间与系统资源(如文件句柄、信号量),但各自拥有独立的栈空间与程序计数器。
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源分配 | 是(独立内存空间、文件描述符等) | 否(共享所属进程资源) |
| 调度单位 | 否 | 是(CPU调度以线程为最小单元) |
| 创建/切换开销 | 高(需分配/回收内存空间) | 低(仅需保存/恢复寄存器与栈) |
| 通信方式 | 管道、消息队列、共享内存等 | 直接读写共享内存(需同步) |
| 独立性 | 高(崩溃不影响其他进程) | 低(一个线程崩溃可能导致整个进程退出) |
例如,在Java中启动一个新进程需调用Runtime.exec(),而创建线程只需实例化Thread对象。若进程内所有线程结束,进程即终止。这也是为何多线程程序更高效——避免了进程间通信的复杂性与高开销。
并发与并行的区别
二者常被混淆,但本质不同:
- 并发(Concurrency):多个任务在同一时间段内交替执行,宏观上看似同时,微观上是分时复用CPU资源。例如:单核CPU上同时运行微信与浏览器。
- 并行(Parallelism):多个任务在同一时刻同时执行,需多核CPU支持。例如:4核CPU上同时运行4个计算任务。
形象比喻:并发像一个人同时点外卖、回邮件、写报告——他切换任务;并行像三个人分工协作——各自专注一摊。多线程是实现并发的手段之一,但并发不必然需要多线程(如协程)。
线程的生命周期
Java线程状态由Thread.State枚举定义,共6种:
新建状态
线程对象已创建,但未调用start()。此时线程未与操作系统线程关联。
运行状态
调用start()后进入此状态,包含“就绪”(等待CPU时间片)与“运行中”(正在执行)。注意:JVM将两者统一为RUNNABLE。
阻塞状态
线程等待进入synchronized代码块/方法时,因锁被占用而阻塞。锁释放后自动恢复。
无限等待
调用Object.wait()、Thread.join()或LockSupport.park()后进入,需其他线程显式唤醒(notify/notifyAll/unpark)。
限期等待
调用sleep()、wait(timeout)、join(timeout)等带超时参数的方法后进入,超时自动恢复。
终止状态
run()方法执行完毕或因未捕获异常退出,线程生命周期结束。
典型面试陷阱:sleep()与wait()都使线程暂停,但sleep()不释放锁(仅释放CPU),而wait()会释放对象锁。因此sleep()常用于“暂停+重试”,wait()用于线程间协作。
二、Java多线程实现方式
继承Thread类
通过继承Thread类并重写run()方法,直接启动线程。代码简洁,但受限于单继承。
// 示例:打印线程名
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程,自动调用run()
}
}
注意:必须用start()启动线程,直接调用run()只是普通方法调用,不会启动新线程!
实现Runnable接口
推荐方式!避免单继承局限,可将任务与线程分离,便于线程池管理。
// 示例:多线程下载文件
public class DownloadTask implements Runnable {
private final String url;
public DownloadTask(String url) { this.url = url; }
@Override
public void run() {
System.out.println("开始下载:" + url);
// 实际下载逻辑...
}
}
public class Main {
public static void main(String[] args) {
new Thread(new DownloadTask("https://example.com/file1.zip")).start();
new Thread(new DownloadTask("https://example.com/file2.zip")).start();
}
}
此方式更符合“组合优于继承”原则,且能复用任务对象(如在线程池中提交同一Runnable实例)。
实现Callable接口
支持返回值与异常处理,常配合FutureTask或线程池使用。
// 示例:计算斐波那契数列
import java.util.concurrent.*;
public class FibonacciTask implements Callable {
private final int n;
public FibonacciTask(int n) { this.n = n; }
@Override
public Long call() {
if (n <= 1) return n;
long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long temp = a + b;
a = b;
b = temp;
}
return b;
}
}
public class Main {
public static void main(String[] args) throws Exception {
FutureTask<Long> task = new FutureTask<>(new FibonacciTask(40));
new Thread(task).start();
System.out.println("结果:" + task.get()); // 阻塞等待结果
}
}
优势:可获取线程执行结果与异常,但需注意get()会阻塞当前线程,建议配合get(timeout, unit)避免无限等待。
线程池:推荐生产环境使用方式
直接创建线程存在三大隐患:
① 频繁创建/销毁线程开销大;
② 缺乏统一管理,可能无限创建导致OOM;
③ 不支持高级功能(如定时、周期执行)。
线程池通过复用线程、控制并发数、提供队列缓冲,完美解决上述问题。
// 示例:创建固定大小线程池处理任务
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService pool = new FixedThreadPool(4); // 核心线程数=4
for (int i = 0; i < 10; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println("处理任务:" + taskId + ",线程:" + Thread.currentThread().getName());
});
}
pool.shutdown(); // 平滑关闭,等待任务完成
}
}
更推荐使用ThreadPoolExecutor自定义参数(见FAQ部分),而非Executors工厂的简化版本,以避免潜在风险。
三、同步机制与线程安全
synchronized关键字详解
底层通过对象监视器(monitor)实现,每个对象有唯一monitor。进入synchronized块前需获取monitor,退出时释放。JDK6后引入三大优化:
无竞争优化
首次获取锁时,将线程ID记录在对象头,后续同一线程进入无需CAS操作。适用于单线程重复加锁场景。
轻度竞争优化
通过CAS将对象头指向栈帧中的锁记录,避免操作系统互斥量开销。仅适用于锁持有时间短的场景。
中度竞争优化
锁竞争时,线程不阻塞而是循环等待(自旋),避免线程挂起/恢复开销。默认自旋10次,可配置。
面试陷阱:synchronized修饰静态方法时,锁对象是Class对象(如MyClass.class),而非实例对象!因此不同实例的静态同步方法仍会互斥。
volatile关键字的作用与局限
volatile提供可见性与禁止指令重排序,但不保证原子性。典型应用:
- 状态标志:如
volatile boolean running = true;用于线程控制 - 双重检查锁单例:避免指令重排序导致的半初始化对象
// 示例:线程安全的单例(DCL)
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 避免重排序
}
}
}
return instance;
}
}
为何要加volatile?因为new Singleton()分三步:
① 分配内存;② 初始化对象;③ 将引用指向内存地址。
若无volatile,JIT可能重排序为①→③→②,导致其他线程看到未初始化的对象。
线程安全的三大核心原则
| 原则 | 实现方式 | 适用场景 |
|---|---|---|
| 无状态设计 | 不存储共享数据(如Spring Bean默认单例无状态) | 工具类、服务层(推荐!) |
| 不可变对象 | 用final修饰字段,无setter方法(如String、Long) | 配置对象、DTO |
| 显式同步 | synchronized、ReentrantLock、Atomic类 | 共享可变状态 |
最佳实践:优先用不可变对象与无状态设计,其次考虑线程局部存储(ThreadLocal),最后才是显式锁。避免“锁滥用”,因锁竞争会成为性能瓶颈。
常见线程安全容器对比
| 容器 | 线程安全? | 并发性能 | 典型场景 |
|---|---|---|---|
| ArrayList | ❌(需Collections.synchronizedList) | 低 | 单线程环境 |
| CopyOnWriteArrayList | ✅ | 高(读无锁) | 读多写少(如监听器列表) |
| HashMap | ❌ | 低 | 单线程环境 |
| ConcurrentHashMap | ✅ | 极高(JDK8 CAS+synchronized) | 高并发读写(推荐!) |
| Vector | ✅ | 低(全方法同步) | 已淘汰,勿用 |
陷阱提示:ConcurrentHashMap的迭代器是弱一致性(fail-safe),遍历时可能不反映最新修改,但不会抛ConcurrentModificationException。
四、线程池深度解析
ThreadPoolExecutor核心参数
构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
| 参数 | 说明 | 典型值 |
|---|---|---|
| corePoolSize | 核心线程数(常驻线程数) | CPU核心数(CPU密集型) 2×CPU核心数(IO密集型) |
| maximumPoolSize | 最大线程数 | 核心数 + 队列容量 |
| workQueue | 任务队列 | LinkedBlockingQueue(无界) ArrayBlockingQueue(有界) |
| handler | 拒绝策略 | AbortPolicy(抛异常) CallerRunsPolicy(回压) |
工作流程:
1. 线程数 < corePoolSize → 创建新线程处理任务
2. 线程数 ≥ corePoolSize & 队列未满 → 入队等待
3. 队列满 & 线程数 < maximumPoolSize → 创建新线程
4. 线程数 = maximumPoolSize & 队列满 → 触发拒绝策略
拒绝策略详解
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 抛出RejectedExecutionException | 监控告警后人工介入 |
| CallerRunsPolicy | 由调用线程(提交任务的线程)执行任务 | 限流保护(提交速度下降) |
| DiscardPolicy | 直接丢弃任务,不抛异常 | 允许任务丢失的场景 |
| DiscardOldestPolicy | 丢弃队列中最旧任务,尝试提交新任务 | 实时系统(如日志处理) |
生产建议:避免使用默认的AbortPolicy,应结合业务设计降级方案(如缓存兜底、异步补偿)。
线程池最佳实践
- 避免使用Executors工厂:其创建的LinkedBlockingQueue无界,可能导致OOM
- 显式命名线程:通过ThreadFactory设置有意义的名称(如"order-pool-1"),便于问题排查
- 合理设置超时:使用
get(timeout, unit)避免无限等待 - 优雅关闭:先shutdown(),再awaitTermination()等待任务完成
// 示例:自定义线程池
import java.util.concurrent.*;
public class CustomThreadPool {
public static final ExecutorService ORDER_POOL = new ThreadPoolExecutor(
8, // corePoolSize
16, // maximumPoolSize
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "order-pool-" + (count++));
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
五、高频面试题精讲
答:线程安全指多线程访问时,代码始终表现出正确的行为(符合预期逻辑),无需额外同步措施。判断三要素:
① 是否共享可变状态;
② 是否被多个线程并发访问;
③ 是否缺乏同步保护。
实战案例:以下代码非线程安全:
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读→改→写
}
public int getCount() { return count; }
}
修复方案:
① 加synchronized;
② 改用AtomicInteger;
③ 无状态设计(count作为局部变量)。
答:死锁指两个或多个线程因争夺资源而互相等待的现象,需满足四个条件:
① 互斥;② 请求与保持;③ 不剥夺;④ 循环等待。
排查方法:
① jstack -l <pid> 查看线程堆栈;
② jcmd <pid> Thread.print 导出死锁报告;
③ 使用JVisualVM图形化监控。
避免策略:
① 按固定顺序获取锁;
② 使用tryLock()设置超时;
③ 减少锁粒度;
④ 用并发工具类替代手动加锁。
答:ThreadLocal为每个线程提供独立变量副本,内部维护ThreadLocalMap(Entry继承WeakReference),以ThreadLocal对象为key。
内存泄漏原因:ThreadLocal引用为弱引用,但Entry的value为强引用。当ThreadLocal被GC回收后,value无法访问却长期驻留内存。
解决方案:
① 每次使用后调用remove();
② 避免在线程池中复用ThreadLocal;
③ 自定义子类重写remove逻辑。
答:JMM定义线程与主内存的关系:
① 所有变量存储于主内存;
② 每条线程有独立工作内存(副本);
③ 线程对变量的操作在工作内存进行。
三大特性:
① 原子性:synchronized保证;
② 可见性:volatile、synchronized保证;
③ 有序性:volatile、synchronized通过内存屏障实现。
关键原则: happens-before原则是判断内存可见性的依据,如:
- 程序顺序原则;
- 监视器锁原则;
- volatile变量原则;
- 线程启动/加入原则。
答:
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 阻塞 | ❌ | ✅ |
| 锁对象 | 无 | 对象监视器 |
| 性能 | 轻量 | 较重(但JDK6后优化) |
典型误区:volatile不能替代synchronized!例如i++需原子性,必须用锁或Atomic类。