在 Java 开发中,多线程是提升程序性能、优化资源利用率的核心技术之一,也是面试中的高频考点。从基础的线程创建到复杂的并发控制,掌握多线程相关概念是成为合格 Java 开发者的必备能力,本文将从核心认知到实践细节,系统拆解多线程知识体系。
一、基础认知:进程与线程的本质区别
在学习多线程前,必须先厘清 "进程" 和 "线程" 的关系 ------ 二者是操作系统中调度资源的基本单位,但粒度不同,这直接决定了它们的核心差异。
1. 进程:程序的一次执行过程
进程是操作系统进行资源分配(如内存、CPU 时间片、文件描述符等)的最小单位。当你通过java HelloWorld运行一个 Java 程序时,操作系统会为其创建独立进程,该进程拥有专属内存空间,进程间资源相互隔离,通信需依赖管道、socket 等专门机制。比如同时打开浏览器和 IDE,二者是两个独立进程,一个崩溃不会直接影响另一个,这就是进程的 "独立性"。
2. 线程:进程内的执行单元
线程是进程内的最小执行单元,也是操作系统任务调度的最小单位。一个进程可包含多个线程,这些线程共享进程的方法区、堆内存等资源,但拥有独立的程序计数器(PC)、虚拟机栈和本地方法栈。以 IDE 为例,"代码编译" 和 "自动保存" 就是两个独立线程,它们共享 IDE 的内存资源,却各自执行不同任务,无需为每个功能单独创建进程,极大节省了资源开销。
3. 核心区别与多线程价值
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 资源独立,分配最小单位 | 共享进程资源,仅私有少量 |
| 调度单位 | 非调度单位,切换开销大 | 调度最小单位,切换开销小 |
| 通信方式 | 跨进程机制,复杂 | 共享内存,简单高效 |
多线程的核心价值在于提升并发能力:CPU 密集型任务(如数据计算)可利用多核资源;IO 密集型任务(如网络请求)可在等待期间调度其他线程;GUI 程序中,子线程处理耗时操作能避免界面 "假死"。
二、核心概念:Java 线程的生命周期与状态转换
Java 线程从创建到销毁会经历多个状态转换,这些状态定义在Thread.State枚举中,共 6 种核心状态,理解它们是排查线程问题的关键。
1. 6 种状态详解
- NEW(新建) :线程对象已创建(如
new Thread()),但未调用start(),未与操作系统线程绑定,此时仅存在于 Java 内存中。 - RUNNABLE(可运行) :包含 "就绪" 和 "运行中" 两种细分状态。调用
start()后线程进入就绪状态,等待 CPU 调度;CPU 分配时间片后进入运行中状态,执行run()方法。 - BLOCKED(阻塞) :线程竞争同步锁(如
synchronized)失败时进入该状态,此时无法参与 CPU 调度,仅当获取到锁后才回归 RUNNABLE。 - WAITING(无限等待) :调用
wait()(无参)、join()(无参)等方法后进入,无时间限制,必须由其他线程通过notify()/notifyAll()主动唤醒。 - TIMED_WAITING(计时等待) :调用
sleep(long)、wait(long)等带时间参数的方法进入,超时后自动唤醒,也可被其他线程提前唤醒。 - TERMINATED(终止) :
run()方法执行完毕或抛出未捕获异常,线程生命周期结束,状态不可逆转,不可再次调用start()。
2. 关键状态转换误区
- 调用
start()并非直接进入运行中,而是进入就绪状态,等待 CPU 调度;直接调用run()只是普通方法调用,不会启动新线程。 sleep()和wait()的核心区别:sleep()不释放同步锁,wait()会释放;sleep()属于 Thread 方法,wait()属于 Object 方法。- 废弃的
stop()方法不可用,强制终止线程会导致资源泄漏,推荐用 "标志位" 或interrupt()优雅终止。
三、线程创建:3 种核心方式及实战对比
Java 提供多种创建线程的方式,核心围绕 "定义任务逻辑" 和 "启动线程",前两种是基础,第三种是企业开发主流。
1. 方式一:继承 Thread 类
Thread 类实现了 Runnable 接口,继承后重写run()定义任务,调用start()启动线程。
java
运行
public class ThreadDemo extends Thread {
@Override
public void run() { // 线程执行逻辑
for (int i = 0;< 5; i++) {
System.out.println(getName() + ": " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
new ThreadDemo().start(); // 启动线程,而非调用run()
new ThreadDemo().start();
}
}
优缺点:实现简单,但 Java 单继承机制限制了灵活性,无法再继承其他类。
2. 方式二:实现 Runnable 接口
Runnable 仅含run()方法,实现该接口定义任务,再传入 Thread 构造器,规避单继承问题。
java
public class RunnableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
Runnable task = new RunnableDemo();
new Thread(task, "线程A").start();
new Thread(task, "线程B").start();
}
}
优缺点 :灵活度高,符合单一职责原则,但run()无返回值,无法抛出受检异常。
3. 方式三:实现 Callable 与 Future 接口
Java 5 引入 Callable 接口,call()方法可返回结果并抛出异常,配合 Future 接口获取执行结果。
java
运行
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Callable<Integer> {
@Override
public Integer call() throws Exception { // 带返回值的任务逻辑
int sum = 0;
for (int i<= 10; i++) { sum += i; Thread.sleep(50); }
return sum;
}
public static void main(String[] args) throws Exception {
Call<Integer> task = new CallableDemo();
<Integer> futureTask<>(task);
new Thread(futureTask).start();
// 获取结果(会阻塞直到线程执行完毕)
System.out.println("计算结果:" + futureTask.get());
}
}
核心优势:支持返回值和异常处理,适合需要获取线程执行结果的场景(如异步计算)。
四、并发控制:线程安全与同步机制
多线程共享资源时,若多个线程同时修改资源,会出现 "线程安全" 问题(如超卖、数据错乱),需通过同步机制保证操作的原子性、可见性和有序性。
1. 核心问题:线程安全的三大特性
- 原子性 :操作不可分割,要么全执行,要么全不执行(如
i++实际是i = i + 1,非原子操作)。 - 可见性:一个线程修改的资源,其他线程能立即感知(避免 CPU 缓存导致的 "脏读")。
- 有序性:线程执行顺序符合代码逻辑,避免 JVM 指令重排序导致的混乱。
2. 同步机制实现方式
-
synchronized 关键字:Java 内置的隐式锁,可修饰方法或代码块,自动实现锁的获取和释放。
java
运行
// 修饰方法 public synchronized void add() { count++; } // 修饰代码块 public void add() { synchronized (this) { count++; } }底层依赖 JVM 的监视器锁(monitor),锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)是性能优化的关键。
-
Lock 接口 :Java 5 引入的显式锁,如 ReentrantLock,需手动调用
lock()和unlock(),支持公平锁、可中断等特性。java
运行
private Lock lock = new ReentrantLock(); public void add() { lock.lock(); try { count++; } finally { lock.unlock(); // 必须在finally中释放锁 } } -
volatile 关键字:保证可见性和有序性,但不保证原子性,适合修饰 "单线程写、多线程读" 的变量(如状态标志位)。
java
运行
private volatile boolean isRunning = true; public void stop() { isRunning = false; } // 其他线程能立即感知状态变化
五、高级工具:线程池与并发容器
频繁创建销毁线程会消耗大量资源,线程池可实现线程复用;JDK 提供的并发容器则能避免手动同步的繁琐。
1. 线程池核心原理
基于 "池化思想",提前创建一定数量的线程,任务提交时直接复用线程,任务结束后线程归池等待,而非销毁。核心参数定义在ThreadPoolExecutor中:
- 核心线程数(corePoolSize):池中长期保留的线程数。
- 最大线程数(maximumPoolSize):池能容纳的最大线程数。
- 空闲时间(keepAliveTime):非核心线程空闲后的存活时间。
- 工作队列(workQueue):存放等待执行的任务(如 LinkedBlockingQueue)。
推荐通过Executors工具类快速创建线程池,但需注意避免newFixedThreadPool等可能导致 OOM 的方法,实际开发更推荐自定义ThreadPoolExecutor。
2. 常用并发容器
- ConcurrentHashMap:线程安全的 HashMap,JDK 1.8 通过 CAS+synchronized 实现,比 HashTable 性能更优。
- CopyOnWriteArrayList:读写分离的 ArrayList,写操作时复制新数组,适合读多写少场景。
- BlockingQueue:阻塞队列,如 ArrayBlockingQueue,常用于生产者 - 消费者模式,自动实现线程间的协作。
六、常见问题:死锁与线程调试
多线程开发中,死锁是典型问题,掌握排查方法是必备技能。
1. 死锁产生条件
需同时满足四个条件:资源互斥、持有并等待、不可剥夺、循环等待。例如两个线程互相持有对方需要的锁:
java
运行
// 线程A持有lock1,等待lock2;线程B持有lock2,等待lock1
Thread A: synchronized (lock1) { synchronized (lock2) {} }
Thread B: synchronized (lock2) { synchronized (lock1) {} }
2. 死锁排查方法
- 命令行工具:
jps获取进程 ID,jstack 进程ID查看线程堆栈,定位死锁线程的锁持有情况。 - IDE 工具:通过 Debug 模式查看线程状态,或使用 VisualVM 等工具可视化分析。
3. 死锁避免方案
- 按固定顺序获取锁(如统一按锁的哈希值升序获取)。
- 定时释放锁(如
tryLock(long time, TimeUnit unit))。 - 减少锁的持有时间,避免嵌套锁。
七、总结与学习建议
Java 多线程的核心是 "平衡性能与安全",学习过程中需注意:
- 夯实基础:理解线程生命周期、进程线程区别等核心概念,避免死记硬背。
- 重视实践:通过代码验证
sleep()与wait()的差异、synchronized与Lock的区别。 - 掌握工具:学会用 jstack、VisualVM 排查死锁、线程阻塞等问题。
- 关注进阶:深入学习 AQS(抽象队列同步器)、ThreadLocal、并发编程模型(如生产者 - 消费者)等内容。