Java多线程核心机制全面解析
作者:故渊ZY
Java 多线程简介
在计算机硬件进入多核时代后,单线程程序已无法充分利用 CPU 资源。多线程(Multithreading)作为并发编程的核心技术,允许程序在同一进程内同时执行多个任务,是提升程序吞吐量、降低响应延迟的关键。从简单的后台任务到复杂的分布式系统,多线程无处不在。本文将从多线程的本质、生命周期、核心机制到实战最佳实践,全面剖析 Java 多线程的设计与应用,帮你掌握并发编程的核心能力。
一、多线程的本质:为什么需要多线程?
1. 多线程与进程、线程的关系
要理解多线程,首先需要明确进程(Process) 与线程(Thread) 的区别与联系 —— 二者是操作系统中调度和资源分配的基本单位,但定位不同:
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源分配 | 操作系统资源分配的基本单位(独立内存空间、文件句柄) | 进程内的执行单元,共享进程资源(堆、方法区),仅拥有独立栈和程序计数器 |
| 切换开销 | 高(需切换内存映射、寄存器状态,耗时约 10-100 微秒) | 低(仅切换栈和程序计数器,耗时约 1-10 微秒) |
| 通信方式 | 复杂(需通过 IPC:管道、socket、共享内存) | 简单(直接访问共享变量,需同步控制) |
| 数量限制 | 少(系统能同时运行的进程数通常不超过千级) | 多(单个进程可创建数千个线程) |
形象类比:进程是 “工厂”,拥有独立的厂房(内存空间)和设备(文件句柄);线程是 “工厂里的工人”,共享厂房和设备,同时有自己的工具(栈空间),多个工人可同时处理不同任务(并发执行)。
多线程的核心价值在于 **“在单个进程内实现并发”**—— 无需创建多个独立进程,即可利用多核 CPU 同时执行任务,大幅降低资源开销和通信成本。
2. 多线程解决的核心问题
多线程并非 “银弹”,但能高效解决两类典型问题,这也是它被广泛应用的原因:
(1)CPU 密集型任务:充分利用多核 CPU
CPU 密集型任务(如数据计算、排序、加密)的核心瓶颈是 CPU 算力。单线程只能利用一个 CPU 核心,而多线程可将任务拆分到多个核心并行执行,减少总耗时。示例:用 4 核 CPU 处理 4 个排序任务,单线程需 4 秒(串行执行),多线程仅需 1 秒(并行执行),理论效率提升 300%。
(2)IO 密集型任务:减少 CPU 空闲时间
IO 密集型任务(如网络请求、文件读写、数据库操作)的核心瓶颈是 IO 等待(CPU 等待数据就绪,如等待网络响应、磁盘 IO 完成)。此时 CPU 处于空闲状态,多线程可让 CPU 在 IO 等待期间处理其他任务,提升 CPU 利用率。示例:单线程处理 10 个网络请求(每个请求耗时 1 秒,其中 0.8 秒是 IO 等待),总耗时 10 秒;多线程处理时,CPU 在 IO 等待期间切换线程,总耗时可降至 1 秒左右,CPU 利用率从 20% 提升至 100%。
3. Java 多线程的特殊性
Java 多线程是内核级线程(1:1 映射模型),即每个 Java 线程对应一个操作系统内核线程:
- 创建 Java 线程时,JVM 会调用操作系统 API(如 Linux 的
pthread_create)创建内核线程; - 线程的调度(CPU 时间片分配、线程切换)由操作系统内核完成,JVM 仅负责线程的创建、销毁和状态管理;
- 相比用户级线程(如 Python 的 GIL 锁限制),Java 多线程能真正利用多核 CPU,但线程数量受限于操作系统(如 Linux 默认单个进程可创建的线程数上限约为 1000-10000,取决于内存大小)。
二、Java 多线程的生命周期:6 种状态与状态转换
Java 线程的生命周期被严格定义在java.lang.Thread.State枚举中,包含 6 种状态。线程在不同状态间的转换遵循固定规则,是理解线程调度和并发控制的基础。
1. 6 种核心状态及含义
| 状态名称 | 核心含义 | 触发场景 |
|---|---|---|
| NEW(新建) | 线程对象已创建(如new Thread()),但未调用start(),未与内核线程关联。 | Thread thread = new Thread(); |
| **RUNNABLE(可运行) | 线程已调用start(),分为 “就绪”(等待 CPU 调度)和 “运行中”(占用 CPU)两种子状态,JVM 不区分这两种子状态。 | thread.start();后,线程等待 CPU 调度或正在执行run()方法 |
| **BLOCKED(阻塞) | 线程等待获取synchronized锁(进入同步块 / 方法时未抢到锁),释放 CPU,等待锁释放。 | 线程 A 执行synchronized方法时,线程 B 尝试进入该方法 |
| **WAITING(无限等待) | 线程通过wait()、join()、LockSupport.park()主动放弃 CPU,无限期等待其他线程唤醒。 | object.wait()(未指定超时)、thread.join()、LockSupport.park() |
| **TIMED_WAITING(计时等待) | 线程通过wait(long)、sleep(long)等方法放弃 CPU,等待指定时间后自动唤醒,或被其他线程提前唤醒。 | Thread.sleep(1000)、object.wait(500)、LockSupport.parkNanos(1_000_000) |
| **TERMINATED(终止) | 线程执行完毕(run()方法正常返回)或因未捕获异常退出,生命周期结束,无法再恢复。 | run()方法执行完毕、线程抛出RuntimeException且未捕获 |
2. 状态转换流程图(核心路径)
NEW → RUNNABLE:调用thread.start()(仅能触发一次,再次调用会抛IllegalThreadStateException) RUNNABLE → BLOCKED:尝试获取synchronized锁失败 BLOCKED → RUNNABLE:其他线程释放synchronized锁,当前线程抢到锁 RUNNABLE → WAITING:调用object.wait()、thread.join()、LockSupport.park() WAITING → RUNNABLE:其他线程调用object.notify()/notifyAll()、LockSupport.unpark() RUNNABLE → TIMED_WAITING:调用Thread.sleep(long)、object.wait(long)、thread.join(long) TIMED_WAITING → RUNNABLE:等待时间到、其他线程调用notify()/unpark() RUNNABLE → TERMINATED:run()执行完毕、未捕获异常终止
关键注意点:
start()与run()的区别:start()是 “启动线程”(触发状态从 NEW→RUNNABLE),run()是 “执行任务逻辑”(仅普通方法调用,不会创建新线程);sleep()与wait()的区别:sleep()不释放synchronized锁(线程仍持有锁),wait()会释放锁;sleep()是Thread类方法,wait()是Object类方法(需在synchronized块中调用);- 终止线程的正确方式:不推荐用
stop()(已废弃,强制终止会导致资源泄漏),应通过volatile变量控制循环退出(如while (!stopFlag) { ... })。
三、Java 多线程的创建方式:4 种核心方法对比
Java 提供了多种创建线程的方式,不同方式在 “灵活性”“返回值”“异常处理” 上存在差异,需根据场景选择。
1. 方式 1:继承 Thread 类(重写 run ())
最基础的创建方式,通过继承Thread类并重写run()方法定义线程任务。
// 1. 继承Thread类
class MyThread extends Thread {
// 2. 重写run():定义线程执行的任务(无返回值,不能抛受检异常)
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread():获取当前执行的线程对象
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500); // 模拟任务耗时,线程进入TIMED_WAITING状态
} catch (InterruptedException e) {
// 捕获中断异常(如其他线程调用interrupt())
System.out.println(Thread.currentThread().getName() + "被中断");
return; // 中断后退出线程
}
}
}
}
// 使用线程
public class ThreadCreate1 {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 4. 设置线程名(便于调试)
thread1.setName("线程A");
thread2.setName("线程B");
// 5. 启动线程(状态从NEW→RUNNABLE)
thread1.start();
thread2.start();
// 注意:直接调用run()不会创建新线程,仅在主线程中执行
// thread1.run(); // 错误用法
}
}输出结果(并发执行,顺序不固定):
线程A: 0
线程B: 0
线程A: 1
线程B: 1
线程A: 2
线程B: 2
...
优缺点:
- 优点:API 简单,直接操作 Thread 对象(如设置优先级、中断);
- 缺点:Java 单继承限制(若类已继承其他类,无法再继承 Thread),
run()无返回值、不能抛受检异常。
2. 方式 2:实现 Runnable 接口(推荐)
通过实现Runnable接口的run()方法定义任务,再将Runnable对象传入Thread构造器,避免单继承限制,是更灵活的方式。
// 1. 实现Runnable接口
class MyRunnable implements Runnable {
// 2. 重写run():定义任务(无返回值,不能抛受检异常)
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 使用线程
public class ThreadCreate2 {
public static void main(String[] args) {
// 3. 创建任务对象(任务与线程分离)
MyRunnable task = new MyRunnable();
// 4. 将任务传入Thread,启动线程(多个线程可共享同一个任务)
Thread thread1 = new Thread(task, "线程C");
Thread thread2 = new Thread(task, "线程D");
thread1.start();
thread2.start();
}
}核心优势:
- 解除单继承限制:类可同时实现
Runnable和其他接口(如Serializable); - 任务与线程分离:一个
Runnable任务可被多个线程共享(如多线程执行同一任务),符合 “单一职责原则”; - 灵活组合:可通过匿名内部类或 Lambda 表达式简化代码(JDK 8+):
// Lambda表达式简化(无需定义MyRunnable类) Thread thread = new Thread(() -> { System.out.println("Lambda线程执行"); }, "Lambda线程"); thread.start();
3. 方式 3:实现 Callable 接口(带返回值)
Runnable的增强版,支持线程执行后返回结果(通过Future获取)和抛出受检异常,适用于需要获取任务结果的场景(如异步计算、耗时任务)。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1. 实现Callable接口(泛型指定返回值类型)
class MyCallable implements Callable<Integer> {
private int num;
public MyCallable(int num) {
this.num = num;
}
// 2. 重写call():有返回值,可抛受检异常
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= num; i++) {
sum += i;
Thread.sleep(100); // 模拟计算耗时
}
return sum; // 返回1~num的和
}
}
// 使用线程
public class ThreadCreate3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3. 创建Callable任务对象
MyCallable task = new MyCallable(10);
// 4. 用FutureTask包装Callable(FutureTask实现了Runnable,可传入Thread)
FutureTask<Integer> futureTask = new FutureTask<>(task);
// 5. 启动线程
new Thread(futureTask, "计算线程").start();
// 6. 获取任务结果(get()是阻塞方法,直到线程执行完毕返回结果)
System.out.println("主线程等待计算结果...");
Integer result = futureTask.get(); // 阻塞等待,直到call()返回
System.out.println("1~10的和:" + result); // 输出:55
// 其他FutureTask方法
System.out.println("任务是否完成:" + futureTask.isDone()); // true
System.out.println("任务是否取消:" + futureTask.isCancelled()); // false
}
}核心特性:
- 带返回值:通过
FutureTask.get()获取call()的返回值,解决Runnable无返回值的问题; - 异常传递:
call()抛出的异常会被封装到ExecutionException中,通过get()的异常链获取; - 可取消:通过
futureTask.cancel(true)取消任务(true表示中断正在执行的线程,false表示仅取消未执行的任务)。
4. 方式 4:线程池(ExecutorService,实战首选)
通过java.util.concurrent.ExecutorService线程池创建线程,避免频繁创建 / 销毁线程的开销(线程池复用线程),是企业级开发的首选方式。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolCreate {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建线程池(根据场景选择线程池类型)
// 固定线程数线程池(适合CPU密集型任务)
ExecutorService fixedPool = Executors.newFixedThreadPool(2);
// 缓存线程池(适合IO密集型任务,线程空闲60秒后销毁)
// ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程池(适合顺序执行任务)
// ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 2. 提交Runnable任务(无返回值)
fixedPool.submit(() -> {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": Runnable任务");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 3. 提交Callable任务(有返回值,返回Future对象)
Future<Integer> future = fixedPool.submit(() -> {
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
Thread.sleep(100);
}
return sum;
});
// 4. 获取Callable任务结果
System.out.println("Callable任务结果:" + future.get()); // 输出:15
// 5. 关闭线程池(重要!否则JVM不会退出)
// shutdown():不再接受新任务,等待已提交任务执行完毕后关闭
fixedPool.shutdown();
// shutdownNow():立即关闭,尝试中断正在执行的任务(可能导致数据不一致)
// fixedPool.shutdownNow();
}
}线程池的核心优势:
- 复用线程:避免频繁创建 / 销毁线程的开销(线程创建需分配栈空间、与内核线程关联,销毁需释放资源);
- 控制并发数:限制同时运行的线程数量,避免线程过多导致 CPU 切换频繁(上下文切换耗时)和内存溢出(每个线程默认栈大小 1MB);
- 任务管理:提供任务队列、拒绝策略(如任务满时丢弃、阻塞)、线程空闲超时销毁等功能,简化任务管理;
- 监控与扩展:可通过
ThreadPoolExecutor自定义线程池(如设置核心线程数、最大线程数、任务队列),支持监控线程池状态(如活跃线程数、完成任务数)。
线程池选择建议:
| 任务类型 | 推荐线程池类型 | 核心参数设置(参考) |
|---|---|---|
| CPU 密集型(计算) | newFixedThreadPool(n) | n = CPU 核心数(Runtime.getRuntime ().availableProcessors ()) |
| IO 密集型(网络 / 文件) | newCachedThreadPool() | 线程数随任务增长,空闲 60 秒销毁 |
| 定时 / 延迟任务 | newScheduledThreadPool(n) | n = 2~4(根据定时任务数量调整) |
| 顺序执行任务 | newSingleThreadExecutor() | 仅 1 个线程,保证任务按提交顺序执行 |
四、多线程的核心挑战:线程安全与同步控制
多线程并发执行时,若多个线程操作共享资源(如共享变量、文件、数据库连接),会出现 “线程安全问题”(数据竞争、结果不一致)。解决线程安全的核心是同步控制—— 确保同一时间只有一个线程操作共享资源。
1. 线程安全问题的根源:可见性、原子性、有序性
Java 内存模型(JMM)定义了线程与主内存之间的交互规则,多线程安全问题本质是 JMM 的三大特性被破坏:
(1)可见性:一个线程修改的共享变量,其他线程无法立即看到
- 原因:CPU 缓存(线程读取变量时先加载到 CPU 缓存,修改后未及时刷回主内存,其他线程读取的仍是缓存中的旧值);
- 示例:
class VisibilityDemo {
private boolean stopFlag = false; // 共享变量(无可见性保证)
public void start() {
new Thread(() -> {
while (!stopFlag) { // 线程1读取的是CPU缓存中的false,即使主线程修改也无法看到
// 循环执行
}
System.out.println("线程停止");
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stopFlag = true; // 主线程修改stopFlag为true,但线程1无法看到
System.out.println("主线程已设置stopFlag为true");
}
public static void main(String[] args) {
new VisibilityDemo().start(); // 线程1不会停止,陷入无限循环
}
}(2)原子性:一个操作不可分割,要么全部执行,要么全部不执行
- 原因:多线程下操作被 CPU 指令拆分(如
count++拆分为 “读取 count→加 1→写入 count” 三步),线程切换导致操作交错; - 示例:
class AtomicityDemo {
private int count = 0; // 共享变量(无原子性保证)
// 非线程安全的方法
public void increment() {
count++; // 原子性问题:多线程下操作交错,导致计数不准确
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
AtomicityDemo demo = new AtomicityDemo();
int threadNum = 2;
Thread[] threads = new Thread[threadNum];
// 2个线程各执行1000次increment()
for (int i = 0; i < threadNum; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 预期结果2000,实际结果可能小于2000(如1850)
System.out.println("最终计数:" + demo.getCount());
}
}(3)有序性:程序执行顺序与代码顺序不一致
- 原因:CPU 指令重排序(为提升性能,CPU 会在不影响单线程结果的前提下,调整指令执行顺序);
- 示例:
class OrderingDemo {
private int a = 0;
private boolean flag = false;
// 线程1执行
public void writer() {
a = 1; // 指令1
flag = true; // 指令2(可能被重排序到指令1之前)
}
// 线程2执行
public void reader() {
if (flag) { // 若flag=true,线程2认为a已被赋值为1
System.out.println(a); // 可能输出0(指令2先执行,指令1未执行)
}
}
}2. 解决线程安全:3 种核心同步机制
(1)synchronized 关键字(内置锁,JVM 层面)
synchronized是 Java 内置的同步机制,通过 “对象监视器(Object Monitor)” 实现,保证 “同一时间只有一个线程执行同步代码块 / 方法”,可同时解决可见性、原子性、有序性问题。
使用方式:
同步方法:锁对象是this(非静态方法)或 “类对象”(静态方法);
// 非静态同步方法(锁是this)
public synchronized void safeIncrement() {
count++; // 原子操作
}
// 静态同步方法(锁是AtomicityDemo.class)
public static synchronized void staticSafeIncrement() {
staticCount++;
}同步代码块:锁对象可自定义(推荐,缩小同步范围,提升性能);
public void safeIncrement() {
synchronized (this) { // 锁对象为this(可替换为其他对象,如private final Object lock = new Object())
count++;
}
}原理:
- 进入同步块时,线程需获取锁(若锁被占用,线程进入
BLOCKED状态,加入锁的等待队列); - 释放锁时,线程释放锁,唤醒等待队列中的一个线程(非公平唤醒,即不按等待顺序);
- JMM 保证:锁释放前,线程修改的共享变量会刷回主内存(可见性);锁获取后,线程会从主内存重新加载共享变量(避免缓存旧值);同时禁止指令重排序(有序性)。
优缺点:
- 优点:使用简单,JVM 自动管理锁的获取与释放(无需手动释放,避免死锁),兼容性好;
- 缺点:锁粒度大(默认锁定整个方法或代码块),不支持中断、超时获取、公平锁,并发性能较低(JDK 1.6 后优化,引入偏向锁、轻量级锁、重量级锁,性能大幅提升)。
(2)Lock 接口(显式锁,JDK 层面)
java.util.concurrent.locks.Lock接口是synchronized的增强版,支持更灵活的锁操作(可中断、超时、公平锁),核心实现类是ReentrantLock(可重入锁)。
使用方式:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockDemo {
private int count = 0;
// 创建Lock对象(默认非公平锁,传入true创建公平锁)
private final Lock lock = new ReentrantLock(true);
public void safeIncrement() {
lock.lock(); // 加锁(若锁被占用,线程进入WAITING状态,加入条件队列)
try {
count++; // 临界区代码(原子操作)
} finally {
lock.unlock(); // 释放锁(必须在finally中,避免异常导致锁未释放)
}
}
// 支持中断的加锁(避免线程无限等待)
public void safeIncrementWithInterrupt() throws InterruptedException {
if (lock.tryLock(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) { // 超时1秒
try {
count++;
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时");
}
}
public int getCount() {
return count;
}
}核心特性:
- 可中断:通过
lock.lockInterruptibly()或tryLock(long, TimeUnit),线程在等待锁时可被中断(Thread.interrupt()); - 超时获取:
tryLock(long, TimeUnit)允许线程在指定时间内获取锁,超时后返回false,避免无限等待; - 公平锁:构造
ReentrantLock(true)创建公平锁,锁释放时优先唤醒等待时间最长的线程(避免线程饥饿),但公平锁性能较低(需维护等待队列顺序); - 条件变量:通过
lock.newCondition()创建Condition对象,支持精细化线程通信(如 “生产者 - 消费者” 模型中,区分 “空队列等待” 和 “满队列等待”)。
Lock vs synchronized:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取 / 释放 | 隐式(JVM 自动管理) | 显式(手动 lock ()/unlock ()) |
| 可中断性 | 不可中断(BLOCKED 状态无法中断) | 可中断(lockInterruptibly ()/tryLock ()) |
| 超时获取 | 不支持 | 支持(tryLock (long)) |
| 公平锁 | 不支持(非公平) | 支持(构造器传入 true) |
| 条件变量 | 支持(wait ()/notify (),1 个条件队列) | 支持(Condition,多个条件队列) |
| 锁状态查询 | 不支持 | 支持(isLocked ()/hasQueuedThreads ()) |
(3)原子类(AtomicXXX,无锁机制)
java.util.concurrent.atomic包下的原子类,基于CAS(Compare and Swap,比较并交换) 实现无锁同步,无需加锁即可保证原子性,性能优于synchronized和Lock(无锁竞争开销)。
核心原子类:
- 基本类型:
AtomicInteger、AtomicLong、AtomicBoolean; - 引用类型:
AtomicReference、AtomicStampedReference(解决 ABA 问题); - 数组类型:
AtomicIntegerArray、AtomicLongArray; - 字段更新器:
AtomicIntegerFieldUpdater(原子更新对象的私有字段)。
使用方式:
import java.util.concurrent.atomic.AtomicInteger;
class AtomicDemo {
// 原子类:保证count++是原子操作
private final AtomicInteger count = new AtomicInteger(0);
public void safeIncrement() {
count.incrementAndGet(); // 原子性的count++(等价于count = count + 1)
}
// 其他原子操作
public void add() {
count.addAndGet(5); // 原子性的count += 5
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicDemo demo = new AtomicDemo();
Thread[] threads = new Thread[2];
for (int i = 0; i < 2; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.safeIncrement();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("最终计数:" + demo.getCount()); // 输出:2000(准确)
}
}CAS 原理:CAS 是一种无锁算法,包含 3 个参数:
- V:共享变量在主内存中的地址;
- A:线程预期的变量旧值;
- B:变量的新值。
执行逻辑:仅当主内存中 V 的值等于 A 时,才将 V 的值更新为 B,否则不操作;整个过程由 CPU 指令cmpxchg保证原子性(无需加锁)。
优缺点:
- 优点:无锁竞争,性能高(尤其在低并发场景),无需担心死锁;
- 缺点:仅支持单个变量的原子操作(无法实现多个变量的原子组合操作),存在 “ABA 问题”(变量从 A→B→A,CAS 误判为未修改,可通过
AtomicStampedReference的版本号解决)。
3. 线程通信:多线程协作的核心机制
线程通信是指多个线程通过共享变量或特定 API 协调执行顺序(如 “线程 A 执行完任务 1 后,线程 B 再执行任务 2”),Java 提供 3 种核心通信方式:
(1)wait ()/notify ()/notifyAll ()(基于 synchronized)
这三个方法是Object类的成员方法,必须在synchronized同步块 / 方法中使用,依赖 “对象监视器” 的等待队列和同步队列实现通信。
- wait():线程释放当前对象的锁,进入该对象的等待队列,状态转为
WAITING,等待被notify()唤醒; - notify():唤醒该对象等待队列中的一个随机线程,使其进入同步队列,竞争锁(状态转为
BLOCKED); - notifyAll():唤醒该对象等待队列中的所有线程,使其进入同步队列竞争锁。
示例:生产者 - 消费者模型(单生产者单消费者)
// 共享缓冲区(存储产品)
class Buffer {
private int[] products = new int[5]; // 缓冲区大小为5
private int count = 0; // 当前产品数量
private int putIndex = 0; // 生产者放入产品的索引
private int takeIndex = 0; // 消费者取出产品的索引
// 生产者放入产品
public synchronized void put(int product) throws InterruptedException {
// 缓冲区满,等待消费者取出(用while避免虚假唤醒)
while (count == products.length) {
System.out.println("缓冲区满,生产者等待");
wait(); // 释放锁,进入等待队列
}
// 放入产品
products[putIndex] = product;
putIndex = (putIndex + 1) % products.length;
count++;
System.out.println("生产者放入产品:" + product + ",当前数量:" + count);
notify(); // 唤醒消费者(缓冲区有产品了)
}
// 消费者取出产品
public synchronized void take() throws InterruptedException {
// 缓冲区空,等待生产者放入
while (count == 0) {
System.out.println("缓冲区空,消费者等待");
wait(); // 释放锁,进入等待队列
}
// 取出产品
int product = products[takeIndex];
takeIndex = (takeIndex + 1) % products.length;
count--;
System.out.println("消费者取出产品:" + product + ",当前数量:" + count);
notify(); // 唤醒生产者(缓冲区有空位了)
}
}
// 生产者线程
class Producer extends Thread {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
buffer.put(i);
Thread.sleep(500); // 模拟生产耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程
class Consumer extends Thread {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
buffer.take();
Thread.sleep(800); // 模拟消费耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试
public class WaitNotifyDemo {
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Producer(buffer).start();
new Consumer(buffer).start();
}
}输出结果(片段):
生产者放入产品:1,当前数量:1
消费者取出产品:1,当前数量:0
缓冲区空,消费者等待
生产者放入产品:2,当前数量:1
消费者取出产品:2,当前数量:0
缓冲区空,消费者等待
生产者放入产品:3,当前数量:1
生产者放入产品:4,当前数量:2
消费者取出产品:3,当前数量:1
...
关键注意点:用while循环判断条件(而非if),避免 “虚假唤醒”—— 线程可能在未被notify()的情况下被唤醒(如操作系统信号中断),while循环会重新检查条件,确保逻辑正确。
(2)Condition 接口(基于 Lock)
Condition是Lock的配套通信工具,通过Lock.newCondition()创建,功能与wait()/notify()类似,但支持更精细的线程分组通信(一个Lock可创建多个Condition,实现不同线程组的独立唤醒)。
示例:用 Condition 实现生产者 - 消费者(多生产者多消费者)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ConditionBuffer {
private int[] products = new int[5];
private int count = 0;
private int putIndex = 0;
private int takeIndex = 0;
private final Lock lock = new ReentrantLock();
// 创建两个Condition:生产者等待队列和消费者等待队列
private final Condition producerCond = lock.newCondition();
private final Condition consumerCond = lock.newCondition();
public void put(int product) throws InterruptedException {
lock.lock();
try {
while (count == products.length) {
System.out.println("缓冲区满,生产者等待");
producerCond.await(); // 生产者进入生产者等待队列
}
products[putIndex] = product;
putIndex = (putIndex + 1) % products.length;
count++;
System.out.println("生产者放入产品:" + product + ",当前数量:" + count);
consumerCond.signal(); // 仅唤醒消费者等待队列的线程(精准唤醒)
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println("缓冲区空,消费者等待");
consumerCond.await(); // 消费者进入消费者等待队列
}
int product = products[takeIndex];
takeIndex = (takeIndex + 1) % products.length;
count--;
System.out.println("消费者取出产品:" + product + ",当前数量:" + count);
producerCond.signal(); // 仅唤醒生产者等待队列的线程
return product;
} finally {
lock.unlock();
}
}
}优势:synchronized的wait()/notify()只能唤醒 “所有线程” 或 “一个随机线程”,而Condition可通过多个条件队列实现 “精准唤醒”(如只唤醒生产者或只唤醒消费者),减少不必要的锁竞争,提升并发性能。
(3)JUC 工具类(CountDownLatch/CyclicBarrier/Semaphore)
java.util.concurrent包提供了更高级的线程通信工具类,简化复杂的协作场景:
- CountDownLatch(倒计时门闩):让一个线程等待其他多个线程执行完毕后再继续(如主线程等待 10 个任务线程执行完);
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int taskNum = 3;
CountDownLatch latch = new CountDownLatch(taskNum);
for (int i = 0; i < taskNum; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 任务完成,倒计时减1
}
}, "任务线程" + i).start();
}
latch.await(); // 主线程等待,直到倒计时为0
System.out.println("所有任务执行完毕,主线程继续");
}
}- CyclicBarrier(循环屏障):让多个线程在某个屏障点等待,直到所有线程都到达后再一起继续(如 3 个线程都到达屏障后,再同时执行后续逻辑);
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int threadNum = 3;
// 屏障动作:所有线程到达后执行
CyclicBarrier barrier = new CyclicBarrier(threadNum, () -> {
System.out.println("所有线程到达屏障,执行屏障动作");
});
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "正在执行");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + "到达屏障");
barrier.await(); // 等待其他线程到达
System.out.println(Thread.currentThread().getName() + "继续执行");
} catch (Exception e) {
e.printStackTrace();
}
}, "线程" + i).start();
}
}
}- Semaphore(信号量):控制同时访问某个资源的线程数量(如限制最多 5 个线程同时访问数据库连接);
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
int permitNum = 2; // 允许同时访问的线程数
Semaphore semaphore = new Semaphore(permitNum);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可(若无许可,线程阻塞)
System.out.println(Thread.currentThread().getName() + "获取许可,访问资源");
Thread.sleep(1000); // 模拟访问资源耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
System.out.println(Thread.currentThread().getName() + "释放许可");
}
}, "线程" + i).start();
}
}
}五、多线程的实战最佳实践:避坑与优化
1. 优先使用线程池,避免手动创建线程
- 手动创建线程的问题:频繁创建 / 销毁线程开销大(线程创建需分配栈空间、与内核线程关联,销毁需释放资源),线程过多导致 CPU 切换频繁(上下文切换耗时约 1~10 微秒),内存占用高(每个线程默认栈大小 1MB);
- 线程池优化建议:
- 避免使用
Executors的默认线程池(如newCachedThreadPool可能创建无限线程,导致 OOM),推荐用ThreadPoolExecutor自定义线程池:// 自定义线程池(推荐) ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // 核心线程数(始终存活,即使空闲) 4, // 最大线程数(核心线程满且队列满时,最多创建的线程数) 60, // 空闲线程存活时间(超过核心线程数的线程,空闲60秒后销毁) TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), // 任务队列(核心线程满时,任务放入队列) Executors.defaultThreadFactory(), // 线程工厂(创建线程) new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(任务满时抛出异常) ); - 根据任务类型设置线程数:CPU 密集型任务(核心线程数 = CPU 核心数),IO 密集型任务(核心线程数 = CPU 核心数 ×2);
- 显式关闭线程池:在应用关闭时调用
executor.shutdown(),避免线程泄漏(尤其在 Web 应用中,容器销毁时线程仍在运行)。
- 避免使用
2. 避免线程安全问题的核心原则
- 最小化共享资源:尽量减少线程间共享的变量,优先使用局部变量(线程私有,无安全问题);
- 使用线程安全的数据结构:如
ConcurrentHashMap(线程安全的 Map)、CopyOnWriteArrayList(线程安全的 List,读多写少场景)、BlockingQueue(线程安全的队列,用于生产者 - 消费者); - 避免锁嵌套:锁嵌套(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)会导致死锁,应按固定顺序获取锁(如始终先获取锁 1,再获取锁 2);
- 使用无锁机制:简单变量操作优先用原子类(
AtomicXXX),避免加锁;复杂场景用ThreadLocal存储线程私有变量(如用户会话信息),避免共享。
3. 优化锁性能的关键技巧
- 缩小同步范围:用
synchronized代码块替代同步方法,仅同步 “临界区”(如只同步count++,而非整个方法);
// 优化前:同步整个方法
public synchronized void process() {
// 非临界区代码(无需同步)
log();
// 临界区代码(需同步)
count++;
}
// 优化后:仅同步临界区
public void process() {
// 非临界区代码(无需同步)
log();
synchronized (this) {
// 临界区代码(需同步)
count++;
}
}- 使用读写锁(ReentrantReadWriteLock):读多写少场景用读写锁,读锁可共享(多个线程同时读),写锁排他(仅一个线程写),提升读操作并发度;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int data;
// 读操作(共享)
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 写操作(排他)
public void writeData(int data) {
writeLock.lock();
try {
this.data = data;
} finally {
writeLock.unlock();
}
}
}- 避免锁竞争:将大锁拆分为小锁(如
ConcurrentHashMap的分段锁,JDK 1.8 后改为 CAS+synchronized),减少锁竞争; - 使用乐观锁:非核心场景用 CAS 替代
synchronized(如用AtomicInteger替代synchronized的计数),避免锁开销。
4. 正确处理线程中断
线程中断是线程间的 “协作信号”,表示 “希望你尽快终止”,而非强制终止线程,正确处理方式:
- 在循环中检查中断状态:通过
Thread.currentThread().isInterrupted()判断是否被中断,若中断则退出;
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断状态(否则后续循环无法感知中断)
Thread.currentThread().interrupt();
break; // 退出循环,终止线程
}
}
}- 不忽略
InterruptedException:sleep()、wait()、join()等方法会抛出InterruptedException,捕获后应恢复中断状态或终止线程,避免上层调用者无法感知中断; - 避免使用
stop():stop()已废弃,强制终止线程会导致资源泄漏(如未关闭文件流、数据库连接)。
5. 避免 ThreadLocal 泄漏
ThreadLocal用于存储线程私有变量(每个线程有独立副本),但使用不当会导致内存泄漏:
- 泄漏原因:
Thread持有ThreadLocalMap,ThreadLocalMap的Entry是弱引用(WeakReference),若ThreadLocal对象被回收,Entry的 key 变为null,但 value 仍被Thread持有,若Thread长期运行(如线程池线程),value 无法回收; - 解决方式:使用完
ThreadLocal后,调用remove()方法清除 value(尤其在线程池场景中);
public class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void process() {
try {
threadLocal.set("线程私有数据");
// 使用数据...
} finally {
threadLocal.remove(); // 清除value,避免泄漏
}
}
}六、常见面试题与误区解析
1. 高频面试题
- Q:start () 和 run () 的区别?A:
start()是 “启动线程”,调用后 JVM 创建内核线程,线程状态从 NEW→RUNNABLE,run()方法在新线程中执行;run()是普通方法,直接调用仅在当前线程中执行,不会创建新线程。 - Q:synchronized 和 Lock 的区别?A:
synchronized是 JVM 内置锁,隐式获取 / 释放,不支持中断、超时、公平锁;Lock是 JDK 显式锁,手动lock()/unlock(),支持中断、超时、公平锁、多条件变量,灵活性更高。 - Q:什么是死锁?如何避免死锁?A:死锁是指多个线程互相持有对方需要的锁,无限等待(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)。避免方式:按固定顺序获取锁、设置锁超时时间(
tryLock())、使用Lock的lockInterruptibly()支持中断、避免锁嵌套。 - Q:ThreadLocal 的原理?为什么会泄漏?A:
ThreadLocal通过Thread的ThreadLocalMap存储线程私有变量,每个线程有独立的ThreadLocalMap。泄漏原因:ThreadLocalMap的Entrykey 是弱引用,ThreadLocal回收后 key 为null,但 value 被Thread持有,若Thread长期运行,value 无法回收;解决方式是使用后调用remove()。 - Q:volatile 关键字的作用?能保证原子性吗?A:
volatile的作用是保证共享变量的可见性(一个线程修改后,其他线程立即看到)和有序性(禁止指令重排序),但不能保证原子性(如volatile int count; count++仍非原子操作,需用原子类或加锁)。
2. 典型误区
- 误区 1:多线程一定比单线程快错误。CPU 密集型任务在单核 CPU 上,多线程因上下文切换开销,可能比单线程慢;IO 密集型任务多线程更有优势。
- 误区 2:线程池的线程数越多越好错误。线程数过多会导致 CPU 上下文切换频繁(每次切换耗时 1~10 微秒),内存占用高(每个线程默认栈大小 1MB),反而降低性能;应根据任务类型(CPU 密集型 / IO 密集型)合理设置线程数。
- 误区 3:synchronized 是重量级锁,性能差错误。JDK 1.6 后对
synchronized进行了优化,引入偏向锁、轻量级锁、重量级锁三级锁机制:无竞争时用偏向锁(仅标记线程 ID),低竞争时用轻量级锁(CAS 自旋),高竞争时用重量级锁(内核态阻塞),性能接近Lock。 - 误区 4:原子类是无锁的,所以没有性能开销错误。原子类基于 CAS 实现,CAS 是自旋操作(未成功时循环重试),高并发场景下自旋次数多,会占用 CPU 资源,性能可能低于
synchronized(重量级锁阻塞时不占用 CPU)。
总结
Java 多线程是并发编程的基石,理解线程的生命周期、同步与通信机制,是写出高效、安全的并发代码的前提。从简单的Thread创建到复杂的线程池管理,从synchronized的基础同步到CAS的无锁优化,每一种机制都对应特定的问题场景。
在实际开发中,无需过度追求 “复杂的并发技巧”,而应遵循 “简单优先” 原则:能用单线程解决的问题,不滥用多线程;能通过原子类或线程池解决的问题,不手动实现复杂的锁逻辑。只有在理解底层原理的基础上,才能灵活应对各种并发场景,让多线程成为提升程序性能的工具,而非引发问题的隐患。
多线程的核心价值,在于让程序 “更高效地利用资源”—— 无论是多核 CPU 的算力,还是 IO 等待时的 CPU 空闲时间。掌握多线程,不仅是掌握一种技术,更是理解 “如何在动态变化的环境中协调多个任务”,这正是优秀工程师的核心能力之一。
到此这篇关于Java多线程核心机制全解析的文章就介绍到这了,更多相关java多线程机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
