Java 多线程
覆盖 Java 多线程从基础概念、核心原理到企业级实战的全量知识点,整合所有对话内容的核心考点、易错点、代码示例,适合日常开发巩固、面试复习使用。
第一章 多线程基础核心概念
1.1 进程与线程的核心区别
核心定义:
- 进程:操作系统资源分配的最小单位,每个进程有独立的内存地址空间,进程间相互隔离。
- 线程:CPU调度执行的最小单位,也叫轻量级进程,一个进程内可包含多个线程,线程共享进程的资源,仅持有少量私有数据(栈、程序计数器)。
表格
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 独立内存空间,创建 / 销毁 / 切换开销极大 | 共享进程资源,仅持有少量私有数据,开销极小 |
| 包含关系 | 操作系统运行基本单位,可包含 1~N 个线程 | 进程的执行单元,无法脱离进程独立存在 |
| 通信方式 | 复杂,需管道、Socket、共享内存、信号量等 IPC 机制 | 简单,直接共享进程堆、方法区资源,通过共享变量即可通信 |
| 健壮性 | 进程间隔离,一个崩溃不影响其他进程 | 共享资源,一个线程崩溃会导致整个进程崩溃 |
| 调度开销 | 切换需更换整个地址空间,Cache 失效,开销极高 | 仅切换上下文,开销极低 |
面试易错点:线程不是越小越好,过多线程会导致 CPU 频繁上下文切换,反而降低性能。
1.2 并行与并发的本质区别
表格
| 特性 | 并发(Concurrent) | 并行(Parallel) |
|---|---|---|
| 核心定义 | 宏观上同一时间段处理多个任务,微观上交替执行 | 微观上同一时刻多个任务同时执行 |
| 硬件依赖 | 单核 CPU 即可实现 | 必须依赖多核 CPU |
| 通俗类比 | 一个人轮流咬 3 个包子 | 3 个人同时各吃 1 个包子 |
面试易错点:高并发设计不等于并行执行,并发是设计能力,并行是硬件执行能力。
第二章 Java 线程的创建与核心方法
2.1 线程创建的 4 种方式
Java 官方定义 2 种基础创建方式,工程中扩展 2 种常用方式,核心区别在于「线程载体」与「业务逻辑」的解耦程度。
方式 1:继承 Thread 类,重写 run () 方法
核心原理:Thread 类是 JDK 提供的具体公共类,实现了 Runnable 接口,可直接实例化。继承后重写 run (),定义线程的业务逻辑。
java
运行
// 自定义线程类
public class MyThread extends Thread {
// 重写run():线程的业务执行入口
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程,JVM自动回调run()
}
}
优缺点:
- 优点:写法简单,可直接操作线程对象
- 缺点:Java 单继承限制,无法再继承其他类;线程与业务逻辑耦合,无法复用
方式 2:实现 Runnable 接口,重写 run () 方法
核心原理:将业务逻辑封装在 Runnable 实现类中,传入 Thread 对象,实现「线程载体」与「业务任务」的解耦。
java
运行
// 自定义任务类
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
// Thread提供了接收Runnable的构造方法,绑定任务
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr); // 同一个任务可被多个线程执行
t1.start();
t2.start();
}
}
优缺点:
- 优点:解耦彻底,避免单继承限制,任务可复用,是基础方式中的首选
- 缺点:无返回值,无法抛出受检异常
方式 3:实现 Callable 接口,配合 FutureTask 使用
核心原理:JDK1.5 引入,支持泛型返回值、抛出受检异常,通过 FutureTask 包装后传入 Thread,可获取线程执行结果。
java
运行
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程执行计算");
return 1 + 1; // 支持返回值
}
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
// 阻塞获取线程执行结果
Integer result = futureTask.get();
System.out.println("执行结果:" + result);
}
}
方式 4:通过线程池创建线程(企业级唯一推荐)
核心原理:通过 ThreadPoolExecutor 手动创建 / Executors 工具类创建线程池,复用线程,避免频繁创建销毁的开销,统一管理线程生命周期。
java
运行
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建单线程池(示例,生产推荐手动创建ThreadPoolExecutor)
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("线程池执行任务"));
executor.shutdown(); // 关闭线程池
}
}
2.2 run () 和 start () 的核心区别
这是面试超高频考点,核心是「线程启动」和「业务执行」的职责分离。
表格
| 对比维度 | start() | run() |
|---|---|---|
| 核心作用 | 启动线程的唯一合法方式,向操作系统申请创建内核线程 | 线程的业务逻辑入口,只是一个普通成员方法 |
| 线程状态 | 调用后线程从 NEW→RUNNABLE,等待 CPU 调度 | 直接调用不会启动新线程,只会在当前调用线程中同步执行 |
| 调用次数 | 一个线程对象只能调用一次,多次调用抛出 IllegalThreadStateException | 可无限次重复调用,和普通方法无区别 |
| 执行特性 | 异步执行,调用后立即返回,run () 在新线程中异步执行 | 同步执行,必须等 run () 执行完毕才会继续往下执行 |
面试易错点:直接调用 run () 不会启动新线程,只有调用 start () 才会真正创建线程。
2.3 Runnable 和 Callable 的核心区别
表格
| 对比维度 | Runnable | Callable |
|---|---|---|
| 核心方法 | void run() | V call() throws Exception |
| 返回值 | 无返回值 | 支持泛型返回值,可获取线程执行结果 |
| 异常处理 | 不能抛出受检异常,只能内部捕获 | 可直接抛出受检异常,异常可被 Future 对象捕获 |
| JDK 版本 | JDK1.0 引入 | JDK1.5 引入 |
| 适用场景 | 无返回结果的异步任务 | 有返回结果的异步任务 |
第三章 Java 线程的生命周期(6 种状态)
Java 线程的状态完全由java.lang.Thread.State枚举定义,共 6 种,状态流转严格遵循 JVM 规范。
3.1 6 种状态的核心定义
- NEW(新建):线程对象已创建,但未调用 start (),未进入调度队列。
- RUNNABLE(可运行):Java 统一封装了操作系统的「就绪 Ready」和「运行 Running」状态。
- 就绪:已调用 start (),等待 CPU 分配时间片
- 运行:已获得 CPU 时间片,正在执行 run () 方法
- BLOCKED(阻塞):线程等待获取 synchronized 排他锁,未拿到锁时进入该状态,拿到锁后回到 RUNNABLE。
- WAITING(无限等待):线程主动进入无限等待,需被其他线程显式唤醒,不会自动超时。触发场景:Object.wait () 无超时、Thread.join () 无超时、LockSupport.park ()
- TIMED_WAITING(计时等待):线程进入限时等待,超时后自动唤醒,也可被提前唤醒。触发场景:Thread.sleep (long)、Object.wait (long)、Thread.join (long)、LockSupport.parkNanos ()
- TERMINATED(终止):线程 run () 执行完毕,或异常退出,生命周期结束,无法再次启动。
3.2 核心状态流转
plaintext
NEW → start() → RUNNABLE
RUNNABLE → 未获取synchronized锁 → BLOCKED → 拿到锁 → RUNNABLE
RUNNABLE → wait/join/park → WAITING → notify/notifyAll/unpark → RUNNABLE
RUNNABLE → sleep/wait(long) → TIMED_WAITING → 超时/唤醒 → RUNNABLE
RUNNABLE → 执行完成/异常退出 → TERMINATED
面试易错点:Thread.sleep () 只会让线程进入 TIMED_WAITING,不会释放锁,也不会进入 BLOCKED 状态。
第四章 多线程核心方法对比与原理
4.1 wait () vs sleep () 全维度对比
面试超高频核心考点,重点关注「锁特性」的差异。
表格
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | java.lang.Object 的实例方法 | java.lang.Thread 的静态方法 |
| 锁的处理 | 必须在 synchronized 同步上下文中调用,调用后会释放持有的对象锁 | 可在任意位置调用,调用后不会释放任何锁 |
| 使用前提 | 必须在 synchronized 代码块 / 方法中使用,否则抛出 IllegalMonitorStateException | 无同步上下文要求 |
| 核心用途 | 线程间通信、协调,实现等待 - 唤醒机制 | 仅让当前线程暂停指定时间,让出 CPU 资源 |
| 唤醒方式 | 需 notify ()/notifyAll () 唤醒,带超时版本可超时自动唤醒 | 到达休眠时间自动唤醒,仅可被 interrupt () 中断唤醒 |
| 中断响应 | 抛出 InterruptedException,响应中断,清除中断标志 | 同左 |
关键易错点:
- sleep()没有无参版本,必须传入时间参数,无参写法编译直接报错。
- sleep()无法被 notify ()/notifyAll () 唤醒,notify 仅能唤醒同一锁对象上调用 wait () 的线程。
4.2 线程中断机制(打断唤醒)
核心本质
thread.interrupt()不会暴力杀死线程,仅会将线程内部的「中断标志位」设置为 true,后续处理由线程自身决定。
两种响应场景
- 线程处于 sleep ()/wait ()/join () 阻塞状态:立即响应中断,抛出 InterruptedException,自动清除中断标志位(重置为 false),这就是「打断唤醒」。
java
运行
Thread sleepThread = new Thread(() -> { try { Thread.sleep(10000); // 休眠10秒 } catch (InterruptedException e) { // 被interrupt()打断唤醒,进入异常处理 System.out.println("线程被中断唤醒"); Thread.currentThread().interrupt(); // 重新设置中断标志位 } }); sleepThread.start(); Thread.sleep(1000); sleepThread.interrupt(); // 发送中断信号,打断休眠 - 线程处于正常运行状态:不会主动抛出异常,需手动通过
Thread.currentThread().isInterrupted()检查标志位,决定是否停止。
4.3 notify () vs notifyAll () 核心区别
两个方法均为 Object 类的本地方法,必须在 synchronized 同步上下文中调用,用于唤醒 wait () 的线程。
- notify():随机唤醒一个处于该对象等待队列中的线程,被唤醒的线程进入锁竞争队列。
- 风险:可能出现「信号丢失」,唤醒错误的线程,导致所有线程无法执行,引发死锁。
- notifyAll():唤醒所有处于该对象等待队列中的线程,所有线程进入锁竞争队列,竞争到锁的线程执行,其余回到等待状态。
- 优势:更安全,无信号丢失风险,是绝大多数场景的首选;缺点是有轻微的锁竞争性能开销。
4.4 保证多线程顺序执行的 3 种方案
方案 1:Thread.join ()(最简单原生方案)
join () 的作用是让当前线程等待调用 join () 的线程执行完毕后,再继续执行。
java
运行
public class ThreadOrderDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> System.out.println("线程1执行"));
Thread t2 = new Thread(() -> System.out.println("线程2执行"));
Thread t3 = new Thread(() -> System.out.println("线程3执行"));
t1.start();
t1.join(); // 主线程等待t1执行完,再启动t2
t2.start();
t2.join(); // 主线程等待t2执行完,再启动t3
t3.start();
}
}
方案 2:单线程池 SingleThreadExecutor
单线程池只有一个工作线程,提交的任务会按提交顺序串行执行,天然保证顺序。
java
运行
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("线程1执行"));
executor.submit(() -> System.out.println("线程2执行"));
executor.submit(() -> System.out.println("线程3执行"));
executor.shutdown();
方案 3:CompletableFuture 串行编排(JDK8 + 企业级推荐)
基于异步回调实现顺序执行,支持复杂的任务编排。
java
运行
CompletableFuture.runAsync(() -> System.out.println("线程1执行"))
.thenRun(() -> System.out.println("线程2执行"))
.thenRun(() -> System.out.println("线程3执行"))
.join();
第五章 线程的优雅停止方案
5.1 废弃方法的坑
绝对禁止使用 stop ()、suspend ()、resume ():这些方法已被 JDK 废弃,会暴力终止线程,强制释放所有锁,导致数据不一致、资源泄漏等严重问题。
5.2 正确停止方案 1:volatile 标记位(非阻塞循环场景)
核心原理:volatile 修饰的布尔标记位保证多线程可见性,线程在循环中持续判断标记位,外部修改后线程正常退出。
java
运行
public class StopThreadDemo {
// volatile保证多线程可见性
private static volatile boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!isStop) {
System.out.println("线程正在运行");
}
System.out.println("线程正常停止");
});
thread.start();
Thread.sleep(1000);
isStop = true; // 修改标记位,通知线程停止
}
}
适用场景:线程执行非阻塞的循环任务,无 sleep/wait 等阻塞操作。
5.3 正确停止方案 2:interrupt 中断机制(通用方案,适配阻塞场景)
核心原理:通过 interrupt () 发送中断信号,同时适配正常运行和阻塞场景,是最通用的优雅停止方案。
java
运行
public class InterruptStopDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
// 非阻塞场景:判断中断标志
while (!Thread.currentThread().isInterrupted()) {
try {
// 阻塞场景:中断会抛出InterruptedException
Thread.sleep(500);
System.out.println("线程正在运行");
} catch (InterruptedException e) {
// 捕获中断异常,收尾后退出
System.out.println("线程被中断,准备退出");
Thread.currentThread().interrupt(); // 重置中断标志
break;
}
}
});
thread.start();
Thread.sleep(2000);
thread.interrupt(); // 发起中断通知
}
}
5.4 正确停止方案 3:Future.cancel ()(线程池 / Callable 场景)
核心原理:线程池提交的任务,通过 Future 的 cancel (true) 方法发起中断,停止任务执行。
java
运行
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("任务正在运行");
}
});
Thread.sleep(1000);
future.cancel(true); // 中断任务,停止线程执行
executor.shutdown();
第六章 synchronized 锁核心原理与实战
6.1 synchronized 的底层本质
Java 中每个对象都自带一把内置锁(监视器锁 Monitor),锁的物理载体存储在对象头的 Mark Word 中。synchronized 的核心是保证同一时刻,只有一个线程能拿到对象的锁,执行同步代码块。
语法语义:synchronized (lock) 表示「当前线程要获取 lock 引用指向的对象的内置锁,获取成功则执行代码块,失败则阻塞等待」。
lock只是一个对象引用变量,不是锁本身,真正的锁是它指向的堆内存中的对象。- 变量名不影响锁的效果,只要指向同一个对象,就是同一把锁。
6.2 锁对象的使用规范
✅ 推荐写法
使用private static final Object作为锁对象,是最稳妥的标准写法:
java
运行
public class SafeDemo {
// 私有:避免外部误用,保证封装性
// static:类级别锁,全局唯一
// final:保证引用不可变,避免锁失效
private static final Object LOCK = new Object();
public void doSomething() {
synchronized (LOCK) {
// 业务逻辑
}
}
}
❌ 禁止 / 不推荐写法
- 用
this当锁:外部可拿到实例对象误用锁,破坏封装性,可能导致死锁。 - 用 String 常量、Integer 缓存对象当锁:JVM 中全局共享 / 缓存复用,极易导致锁失效、死锁。
- 锁对象引用可变:比如
lock = new Object()重新赋值,导致不同线程用不同的锁,完全失效。
6.3 细粒度锁实现:lockMap 的核心用法
核心作用
lockMap本质是ConcurrentHashMap<业务ID, 锁对象>,是「业务 ID→锁对象」的映射表,实现细粒度锁,避免全局锁的性能瓶颈。
解决的痛点
全局锁会让所有业务请求串行执行,哪怕业务之间完全无关;而 lockMap 为每个业务(商品、账户、请求)创建独立的锁,只有同一业务的请求才会竞争锁,不同业务完全并行,极大提升并发性能。
标准代码实现
java
运行
import java.util.concurrent.ConcurrentHashMap;
public class StockService {
// 商品ID → 剩余库存
private final ConcurrentHashMap<Long, Integer> stockDB = new ConcurrentHashMap<>();
// lockMap:商品ID → 对应商品的锁对象
private final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>();
// 初始化商品库存
public void initStock(Long productId, Integer initStock) {
stockDB.put(productId, initStock);
lockMap.putIfAbsent(productId, new Object());
}
// 扣减库存核心方法
public boolean deductStock(Long productId, Integer buyNum) {
// 1. 获取当前商品的专属锁对象
Object productLock = lockMap.get(productId);
if (productLock == null) {
throw new RuntimeException("商品不存在");
}
// 2. 只锁当前商品,不影响其他商品
synchronized (productLock) {
// 3. 加锁后再次校验库存
Integer currentStock = stockDB.get(productId);
if (currentStock == null || currentStock < buyNum) {
System.out.println("库存不足");
return false;
}
// 4. 原子扣减库存
stockDB.put(productId, currentStock - buyNum);
System.out.printf("商品%d扣减成功,剩余库存%d%n", productId, currentStock - buyNum);
return true;
}
}
}
核心方法说明
lockMap.putIfAbsent(key, value):原子操作,仅当 key 不存在时才存入,保证同一个业务 ID 永远对应同一个锁对象,避免多线程下锁覆盖。lockMap.get(key):根据业务 ID 获取对应的锁对象,用于 synchronized 加锁。
6.4 企业级 5 大实战场景
- 电商库存扣减:按商品维度加细粒度锁,避免超卖,保证并发安全。
- 账户转账:固定加锁顺序(按账户 ID 从小到大),避免死锁,保证资金一致性。
- 接口防重复提交:按幂等号加锁,配合双重校验,避免重复提交。
- 双重检查锁(DCL)单例模式:配合 volatile 禁止指令重排,实现线程安全的懒汉式单例。
- 本地缓存并发读写:加锁保证缓存读写的原子性,避免脏读、并发修改异常。
第七章 volatile 关键字核心原理
7.1 Java 内存模型(JMM)与可见性问题
JMM 将内存分为两类:
- 主内存:所有线程共享,存储共享变量的 “真身”。
- 工作内存:每个线程私有,线程读写变量必须先把主内存的变量复制到工作内存,操作后再择机刷回主内存。
可见性问题:一个线程修改了主内存的共享变量,其他线程看不到,依然使用自己工作内存的旧副本,导致逻辑错误。
7.2 volatile 的两大核心作用
1. 保证共享变量修改的立即可见性
volatile 对 JVM 强制约束:
- 线程修改 volatile 变量后,必须立即强制刷回主内存。
- 线程每次读取 volatile 变量时,必须强制从主内存重新读取最新值,不能使用工作内存的旧副本。
这也是flag标记位必须用 volatile 修饰的核心原因:保证主线程修改 flag 后,子线程能立即看到,及时停止线程,避免死循环。
2. 禁止指令重排序
JVM 为了优化性能,会在不改变单线程语义的前提下,打乱代码执行顺序(指令重排序)。volatile 会通过「内存屏障」禁止 JVM 对该变量相关的代码进行重排序,保证代码按书写顺序执行。
经典应用场景:双重检查锁(DCL)单例模式,禁止对象初始化的指令重排,避免多线程拿到半初始化的对象。
7.3 关键易错点
volatile只能保证可见性和禁止指令重排,无法保证原子性。比如volatile int count; count++,count++ 是「读 - 改 - 写」三步操作,多线程下依然会出现并发安全问题,需要加锁保证原子性。
第八章 JVM 栈内存与多线程
8.1 栈内存与栈帧的核心定义
- 栈内存(虚拟机栈):每个线程私有的内存区域,线程创建时自动分配,线程销毁时自动释放,不存在线程安全问题。
- 栈帧:每个方法执行时,都会创建一个栈帧,存储方法的局部变量表、操作数栈、方法出口等信息。方法调用入栈,方法执行完毕出栈。
8.2 栈内存大小与线程数的关系
JVM 的栈内存大小通过-Xss参数设置,默认通常为 1024k(1M)。
- 每个线程启动时,JVM 都会为它分配一块固定大小的栈内存。
- 机器 / 进程的可用内存是固定的,单个线程的栈内存越大,能创建的总线程数就越少。
示例:
- 进程可用内存 512M,栈内存默认 1M,理论最大线程数≈512 个。
- 若把栈内存改为 2M,理论最大线程数直接减半≈256 个。
8.3 栈内存不是越大越好的核心原因
- 限制总线程数:栈内存越大,能创建的线程数越少,高并发场景下会导致无法创建新线程,抛出
OutOfMemoryError: unable to create new native thread异常。 - 内存浪费:绝大多数业务方法不需要太大的栈内存,默认 1M 完全足够,设置过大只会浪费内存资源。
- 栈溢出风险降低有限:
StackOverflowError通常是方法递归调用过深导致,单纯增大栈内存只能缓解,无法根治,反而会带来上述问题。
最佳实践:无特殊需求(如深度递归、大量局部变量),保持 JVM 默认栈内存大小即可;高并发场景可适当调小栈内存,提升可创建的线程数上限。
面试高频考点速记
- 进程是资源分配最小单位,线程是 CPU 调度最小单位。
- 启动线程只能用 start (),直接调用 run () 不会创建新线程,使用主线程执行。
- wait () 会释放锁,sleep () 不会释放锁;sleep 只能被中断或超时唤醒,不能被 notify 唤醒。
- 停止线程优先用 interrupt 中断机制,或 volatile 标记位,禁止用 stop ()。
- volatile 保证可见性、禁止指令重排,不保证原子性。
- synchronized 锁的是对象,不是代码;优先用细粒度锁,避免全局锁。
- 栈内存不是越大越好,栈内存越大,能创建的线程数越少。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)