多线程并发是Java后端核心重难点、面试压轴考点,也是服务端高并发、高可用架构的底层基石。日常业务异步处理、接口限流、批量任务、并行计算、框架底层(Spring、MyBatis、线程池)全部依赖多线程机制实现。
本文全覆盖核心考点:线程创建(Thread/Runnable)、synchronized锁、Lock&AQS、volatile、ThreadLocal、线程池Executor、CountDownLatch/CyclicBarrier并发工具、Fork/Join分支合并,搭配底层原理、完整实战代码、高频坑点、工程规范、面试标准答案,零基础可学、面试可直接背诵、项目可直接落地。
一、多线程核心基础概述
1.1 进程与线程区别
-
进程:系统资源分配的最小单位,独立内存、独立资源,进程间隔离性强、通信成本高
-
线程:进程内的执行单元、CPU调度最小单位,共享进程资源,轻量级、切换成本低
1.2 并发与并行
-
并发:单核CPU快速切换多个线程,宏观同时执行、微观交替执行
-
并行:多核CPU同时执行多个线程,真正意义上的同时运行
1.3 线程五大生命周期(面试必背)
新建(New) → 就绪(Runnable) → 运行(Running) → 阻塞(Blocked/Waiting) → 终止(Terminated)
二、线程创建方式:Thread vs Runnable
Java最基础的两种线程创建方式,也是所有多线程的起点,核心区别在于是否支持资源共享、是否面向接口编程。
2.1 继承Thread类
继承Thread类,重写run()方法,单继承限制,无法复用资源。
// 继承Thread类创建线程
public class ThreadDemo extends Thread {
@Override
public void run() {
// 线程执行任务
System.out.println("线程执行:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
2.2 实现Runnable接口(推荐)
实现Runnable接口,重写run()方法,规避单继承限制,支持多线程共享任务资源,工程开发首选。
// 实现Runnable接口
public class RunnableDemo implements Runnable {
private int count = 10;
@Override
public void run() {
while (count > 0) {
System.out.println(Thread.currentThread().getName() + " 剩余数量:" + count--);
}
}
public static void main(String[] args) {
// 同一个任务对象,多个线程共享资源
RunnableDemo task = new RunnableDemo();
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}
2.3 Thread与Runnable核心区别
-
继承Thread:不能继承其他类,每个线程独立资源,无法共享任务
-
实现Runnable:支持多实现、资源共享、解耦任务与线程,符合面向接口编程
三、同步锁:synchronized(内置锁)
synchronized是Java原生内置锁,解决多线程共享资源的线程安全问题,保证原子性、可见性、有序性,无需手动解锁,底层由JVM实现。
3.1 三种使用方式
-
修饰普通方法:锁当前对象this
-
修饰静态方法:锁当前类Class对象(全局锁)
-
修饰代码块:锁自定义对象(细粒度锁,推荐)
3.2 实战代码(解决线程不安全)
public class SyncDemo {
private static int num = 0;
// 同步代码块,保证计数安全
public static void add() {
synchronized (SyncDemo.class) {
num++;
}
}
public static void main(String[] args) throws InterruptedException {
// 开启10个线程累加1000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
Thread.sleep(2000);
// 最终结果固定10000
System.out.println("累加结果:" + num);
}
}
3.3 底层锁升级机制(面试核心)
JDK1.6优化后,synchronized不再是重量级锁,具备锁升级机制,性能大幅提升:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
-
偏向锁:单线程竞争,无锁开销
-
轻量级锁:少量线程交替竞争,自旋CAS
-
重量级锁:大量线程竞争,阻塞等待
四、Lock锁与AQS核心原理
Lock是JDK5提供的显式锁,功能比synchronized更强大、灵活,支持超时锁、可中断锁、公平锁,底层基于AQS实现。
4.1 AQS核心原理
AQS(AbstractQueuedSynchronizer):抽象队列同步器,是所有Lock锁、并发工具的底层基石。
-
核心变量:state状态值(0无锁、1加锁、>1重入锁)
-
核心结构:双向阻塞队列,存储所有等待线程
-
核心机制:CAS自旋抢锁 + 队列阻塞排队
4.2 ReentrantLock可重入锁实战
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private static int num = 0;
// 创建显式锁
private static final Lock LOCK = new ReentrantLock();
public static void add() {
LOCK.lock(); // 手动加锁
try {
num++;
} finally {
LOCK.unlock(); // 手动解锁,必须放finally
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
Thread.sleep(2000);
System.out.println("累加结果:" + num);
}
}
4.3 synchronized vs ReentrantLock 终极对比
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 锁类型 | 内置隐式锁,JVM管控 | 显式锁,代码手动管控 |
| 锁机制 | 自动加锁解锁 | 手动lock/unlock,必须finally解锁 |
| 公平锁 | 默认非公平,不支持修改 | 支持公平/非公平锁切换 |
| 高级功能 | 无 | 可中断、超时锁、条件队列 |
| 性能 | JDK1.6后优化,性能持平 | 高并发场景性能更优 |
五、volatile 关键字(轻量级并发)
volatile是轻量级并发关键字,核心作用:保证可见性、禁止指令重排序,不保证原子性,无锁开销,适合状态标记、读写分离场景。
5.1 三大核心特性
-
保证可见性:一个线程修改变量,其他线程立即感知最新值,禁止缓存
-
禁止指令重排序:遵循内存屏障规则,解决DCL单例模式重排序BUG
-
不保证原子性:复合操作(i++)非线程安全,无法替代锁
5.2 可见性实战代码
public class VolatileDemo {
// 不加volatile,程序死循环;加volatile,主线程可见变更,程序结束
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 循环监听flag状态
}
}).start();
Thread.sleep(1000);
flag = false;
System.out.println("主线程修改flag为false,子线程结束循环");
}
}
5.3 高频面试坑点
-
volatile只能修饰变量,不能修饰方法、类
-
无法保证原子性,i++、复合运算仍存在线程安全问题
-
适合读多写少场景,纯粹状态标记
六、ThreadLocal 线程本地变量
ThreadLocal:线程本地存储,为每个线程单独分配独立变量副本,线程间数据完全隔离,彻底解决多线程变量共享冲突问题。
6.1 核心原理
每个Thread内部维护一个ThreadLocalMap,key为ThreadLocal对象,value为线程私有数据,不同线程的Map相互独立,互不干扰。
6.2 实战代码
public class ThreadLocalDemo {
// 创建线程本地变量
private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
LOCAL.set("线程A数据");
System.out.println(Thread.currentThread().getName() + ":" + LOCAL.get());
}, "线程A").start();
new Thread(() -> {
LOCAL.set("线程B数据");
System.out.println(Thread.currentThread().getName() + ":" + LOCAL.get());
}, "线程B").start();
}
}
6.3 核心坑点与最佳实践(工程必看)
-
内存泄漏:线程池场景线程复用,不remove会导致数据残留、内存溢出
-
使用规范:使用完毕必须调用
LOCAL\.remove\(\)清除数据 -
适用场景:用户上下文、请求参数、事务信息、独立线程缓存
七、线程池 Executor(工程核心)
线程池是企业开发必须使用的线程创建方式,避免频繁创建销毁线程的开销,统一管理线程、管控并发、提升系统吞吐量。
7.1 核心优势
-
降低资源消耗:复用线程,避免频繁创建销毁
-
提高响应速度:线程就绪,可直接执行任务
-
统一管控:统一调度、监控、拒绝策略,避免并发爆炸
7.2 七大核心参数(面试必考)
-
核心线程数corePoolSize:常驻线程,空闲不销毁
-
最大线程数maximumPoolSize:线程池允许的最大线程数量
-
空闲超时时间keepAliveTime:非核心线程空闲超时销毁时间
-
时间单位unit:超时时间单位
-
任务队列workQueue:存储等待执行的任务
-
线程工厂threadFactory:创建线程,自定义线程名
-
拒绝策略handler:任务满负荷时的拒绝规则
7.3 手动创建线程池(阿里规范,禁止Executors)
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 手动创建线程池(工程标准写法)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 提交任务
for (int i = 0; i < 8; i++) {
int index = i;
pool.execute(() -> {
System.out.println("执行任务:" + index + ",线程:" + Thread.currentThread().getName());
});
}
// 关闭线程池
pool.shutdown();
}
}
7.4 四大拒绝策略
-
AbortPolicy:默认策略,直接抛出异常
-
CallerRunsPolicy:调用者线程执行任务
-
DiscardPolicy:直接丢弃任务,无异常
-
DiscardOldestPolicy:丢弃队列最旧任务,执行新任务
八、并发工具类:CountDownLatch / CyclicBarrier
JUC并发工具用于多线程任务协同、批量任务控制,是业务异步、批量处理高频工具。
8.1 CountDownLatch(倒计时计数器)
特性:一次性计数器,线程倒计时归零后,唤醒等待线程,不可复用。
场景:主线程等待所有子线程任务执行完毕再执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器初始值5
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 任务执行完成");
latch.countDown(); // 计数器-1
}).start();
}
latch.await(); // 主线程等待计数器归零
System.out.println("所有子线程执行完毕,主线程收尾");
}
}
8.2 CyclicBarrier(循环栅栏)
特性:可循环复用屏障,所有线程到达屏障点后统一放行,支持重复使用。
场景:多线程分批任务、多人组队同步执行。
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 凑齐5个线程统一执行,可循环复用
CyclicBarrier barrier = new CyclicBarrier(5, () -> {
System.out.println("凑齐5个线程,统一放行执行任务");
});
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障点");
barrier.await(); // 等待凑齐数量放行
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3 两者核心区别
-
CountDownLatch:一次性、递减归零、主线程等待子线程
-
CyclicBarrier:可循环复用、凑数放行、线程互相等待
九、Fork/Join 分支合并框架
Fork/Join是JDK7推出的并行计算框架,核心思想:分而治之,将大任务拆分为小任务并行执行,最终合并结果,适合海量数据计算、递归任务。
9.1 核心原理
-
Fork拆分:大任务递归拆分为独立小任务
-
Join合并:等待所有子任务执行完毕,合并最终结果
-
工作窃取算法:空闲线程窃取繁忙线程任务,最大化利用CPU
9.2 实战代码(1-100累加计算)
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
// 自定义累加任务
class SumTask extends RecursiveTask<Integer> {
private int start;
private int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
// 阈值:差值小于10直接计算
if (end - start <= 10) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
// 拆分任务
int mid = (start + end) / 2;
SumTask left = new SumTask(start, mid);
SumTask right = new SumTask(mid + 1, end);
left.fork();
right.fork();
// 合并结果
return left.join() + right.join();
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> task = pool.submit(new SumTask(1, 100));
System.out.println("1-100累加结果:" + task.get());
pool.shutdown();
}
}
十、多线程工程坑点 & 最佳实践
-
禁止手动创建线程:所有业务线程统一使用线程池管理
-
禁止使用Executors工具类:避免无界队列、超大线程数导致OOM
-
volatile不做原子运算:仅用于状态标记,复合运算必须加锁
-
ThreadLocal必须remove:线程池复用线程,防止内存泄漏和数据串扰
-
锁粒度最小化:优先同步代码块,减少锁持有时间,提升并发性能
-
高并发优先Lock:竞争激烈场景ReentrantLock性能优于synchronized
-
Fork/Join适合密集计算:不适合IO阻塞任务,浪费CPU资源
十一、全文核心总结(面试必背)
-
线程创建:Thread继承、Runnable实现,优先Runnable解耦共享
-
synchronized:内置锁,自动加解锁,锁升级机制,保证三大特性
-
Lock&AQS:显式锁,基于队列同步器,功能更强、高并发更优
-
volatile:轻量级,保证可见性、禁止重排,不保证原子性
-
ThreadLocal:线程数据隔离,解决线程安全,注意内存泄漏
-
线程池:七大参数、四大拒绝策略,统一线程管理,工程必备
-
并发工具:CountDownLatch一次性倒计时、CyclicBarrier循环栅栏
-
Fork/Join:分而治之、工作窃取,适合并行批量计算任务
十二、高频面试简答题
-
线程和进程区别? 进程是资源分配单位,线程是CPU调度单位,线程共享进程资源、开销更低。
-
synchronized和Lock区别? 前者隐式自动锁,后者显式手动锁,Lock支持公平锁、超时锁、可中断,功能更强大。
-
volatile能否保证线程安全? 不能,仅保证可见性和有序性,不保证原子性,复合操作仍不安全。
-
ThreadLocal原理与坑点? 线程独立Map存储数据,线程隔离;线程池场景不清除会导致内存泄漏、数据串扰。
-
为什么禁止使用Executors? 部分线程池使用无界队列,任务堆积会导致内存溢出,无法管控并发数量。
-
CountDownLatch和CyclicBarrier区别? 前者一次性倒计时,主线程等待子线程;后者可循环复用,线程互相等待。
-
AQS核心原理? 基于state状态值+双向阻塞队列,CAS抢锁、失败入队阻塞,是JUC锁的底层核心。
-
Fork/Join工作窃取算法? 空闲线程主动窃取其他线程未执行任务,均衡负载,提升CPU利用率。
(注:文档部分内容可能由 AI 生成)