Java 多线程从入门到精通
本文涵盖 Java 多线程 28 个核心知识点,从基础概念、线程创建、线程控制,到同步机制、锁机制、线程池、线程安全问题,全体系覆盖。
多线程 01:概述
1.1 什么是多线程
多线程是指 在一个程序中同时运行多个线程,每个线程执行不同的任务。线程是进程内的执行单元,一个进程可以包含多个线程,共享进程的内存资源(如堆内存、文件句柄等)。
1.2 多线程的核心价值
- 提高程序执行效率:充分利用 CPU 多核资源,避免单线程阻塞导致程序闲置。
- 提升用户体验:例如 GUI 程序中,后台线程执行耗时任务,主线程保持响应。
- 支持并发场景:如电商秒杀、聊天系统、日志处理等高并发场景。
1.3 进程与线程的区别
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 操作系统分配独立内存空间 | 共享所属进程的内存,仅拥有独立栈、程序计数器 |
| 切换开销 | 进程切换开销大(需切换内存、CPU 等资源) | 线程切换开销小(仅切换栈、程序计数器) |
| 依赖关系 | 进程是资源分配的基本单位 | 线程是 CPU 调度的基本单位,依赖进程存在 |
多线程 02:线程、进程、多线程
2.1 核心概念拆解
- 进程:操作系统进行资源分配和调度的独立单位,一个应用程序对应一个进程(如 QQ 进程、浏览器进程)。
- 线程:进程内的执行路径,是 CPU 调度的最小单位,一个进程可包含多个线程。
- 多线程:同一进程中创建多个线程,并发执行不同任务,实现 “同时处理多件事”。
2.2 多线程的执行原理
CPU 通过 时间片轮转 调度线程:给每个线程分配短暂的 CPU 时间片,线程在时间片内执行任务,时间片耗尽后切换至下一个线程,宏观上呈现 “多线程同时执行” 的效果。
2.3 多线程的使用场景
- 后台任务:如日志写入、文件下载、数据解析。
- 前端交互:如 GUI 程序的按钮点击、页面刷新。
- 高并发业务:如电商订单处理、秒杀系统、消息推送。
多线程 03:继承 Thread 类
3.1 核心原理
Thread 类是 Java 中表示线程的核心类,继承 Thread 类并重写 run() 方法,即可创建自定义线程。
3.2 实现步骤
- 定义类继承
Thread类。 - 重写
run()方法,在方法中编写线程执行的任务逻辑。 - 创建线程对象,调用
start()方法启动线程(不能直接调用 run () 方法,否则不会启动新线程)。
3.3 代码示例
// 自定义线程类:继承Thread
class MyThread extends Thread {
@Override
public void run() {
// 线程任务:循环打印线程名称
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
try {
// 休眠100ms,模拟耗时任务
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadExtendsDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 设置线程名称
thread1.setName("线程A");
thread2.setName("线程B");
// 启动线程
thread1.start();
thread2.start();
}
}
3.4 注意事项
- 继承
Thread类后,类无法再继承其他类(Java 单继承特性)。 run()方法是线程的任务入口,start()方法会启动新线程并执行run()。
多线程 04:网图下载(线程实战)
4.1 实战需求
使用多线程批量下载网络图片,通过 Thread 类实现,模拟真实场景中的多线程文件下载。
4.2 核心依赖
Java 网络编程需使用 URL、URLConnection 类,IO 流需使用 FileOutputStream、InputStream 类。
4.3 代码示例
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
// 下载线程类
class DownloadThread extends Thread {
// 网络图片地址
private String imageUrl;
// 本地保存路径
private String savePath;
public DownloadThread(String imageUrl, String savePath) {
this.imageUrl = imageUrl;
this.savePath = savePath;
}
@Override
public void run() {
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
// 设置请求头,模拟浏览器
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
// 获取输入流
InputStream inputStream = connection.getInputStream();
// 写入本地文件
FileOutputStream outputStream = new FileOutputStream(savePath);
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
// 关闭流
outputStream.close();
inputStream.close();
System.out.println(Thread.currentThread().getName() + ":图片下载完成 -> " + savePath);
} catch (Exception e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + ":图片下载失败");
}
}
}
// 测试类
public class ImageDownloadDemo {
public static void main(String[] args) {
// 测试用的网图地址(可替换为真实地址)
String url1 = "https://img-blog.csdnimg.cn/202401011200001.png";
String url2 = "https://img-blog.csdnimg.cn/202401011200002.png";
// 创建下载线程
DownloadThread thread1 = new DownloadThread(url1, "image1.png");
DownloadThread thread2 = new DownloadThread(url2, "image2.png");
// 启动线程
thread1.start();
thread2.start();
}
}
4.4 运行说明
- 需替换
imageUrl为真实的网络图片地址。 - 图片会保存到项目根目录下,可直接查看。
多线程 05:实现 Runnable 接口
5.1 核心优势
实现 Runnable 接口是创建线程的 推荐方式,解决了继承 Thread 类的单继承限制,同时可实现资源共享。
5.2 实现步骤
- 定义类实现
Runnable接口。 - 重写
run()方法,编写线程任务逻辑。 - 创建
Runnable实现类对象,将其作为参数传入Thread构造器。 - 调用
Thread对象的start()方法启动线程。
5.3 代码示例
// 自定义任务类:实现Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class RunnableDemo {
public static void main(String[] args) {
// 创建任务对象
MyRunnable myRunnable = new MyRunnable();
// 创建线程对象,绑定任务
Thread thread1 = new Thread(myRunnable, "线程A");
Thread thread2 = new Thread(myRunnable, "线程B");
// 启动线程
thread1.start();
thread2.start();
}
}
5.4 与继承 Thread 的对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 继承 Thread | 代码简单,直接获取当前线程 | 单继承限制,无法继承其他类 |
| 实现 Runnable | 无单继承限制,支持资源共享 | 需通过 Thread 包装,稍繁琐 |
多线程 06:初识并发问题
6.1 什么是并发问题
多线程同时操作 共享资源 时,因线程执行的随机性(CPU 时间片切换),导致数据结果不符合预期,即并发安全问题。
6.2 典型案例:卖票
多个窗口同时卖票,若未处理并发,会出现超卖、重复卖票问题。
6.3 代码示例(问题演示)
class TicketRunnable implements Runnable {
// 共享票源
private int ticketNum = 100;
@Override
public void run() {
while (ticketNum > 0) {
try {
// 模拟售票耗时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 售票
System.out.println(Thread.currentThread().getName() + ":售出第" + ticketNum + "张票");
ticketNum--;
}
}
}
public class ConcurrencyProblemDemo {
public static void main(String[] args) {
TicketRunnable ticketTask = new TicketRunnable();
// 创建三个窗口线程
Thread window1 = new Thread(ticketTask, "窗口1");
Thread window2 = new Thread(ticketTask, "窗口2");
Thread window3 = new Thread(ticketTask, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
6.4 问题分析
- 运行后会出现 重复售票(多个窗口同时售出同一张票)、超卖(ticketNum 变为负数)的问题。
- 原因:多个线程同时读取、修改
ticketNum,未保证操作的原子性。
多线程 07:龟兔赛跑(线程控制实战)
7.1 实战需求
模拟龟兔赛跑场景,通过线程休眠控制线程执行速度,实现 “兔子跑得快但睡觉,乌龟慢但一直跑” 的效果。
7.2 代码示例
class RaceThread implements Runnable {
// 胜利者
private static String winner;
@Override
public void run() {
String name = Thread.currentThread().getName();
// 模拟赛跑:100步
for (int step = 1; step <= 100; step++) {
// 兔子线程:每走50步休眠100ms(睡觉)
if (name.equals("兔子") && step % 50 == 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 乌龟线程:每走10步休眠20ms(慢走)
if (name.equals("乌龟")) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + ":走了" + step + "步");
}
// 判断是否到达终点
if (winner == null) {
winner = name;
System.out.println("\n===== 比赛结果 =====");
System.out.println("胜利者:" + winner);
}
}
}
public class TortoiseRabbitRace {
public static void main(String[] args) {
RaceThread raceTask = new RaceThread();
Thread rabbit = new Thread(raceTask, "兔子");
Thread tortoise = new Thread(raceTask, "乌龟");
rabbit.start();
tortoise.start();
}
}
7.3 运行结果
- 兔子中途睡觉,最终乌龟获胜(或兔子获胜,取决于线程调度)。
- 直观展示了线程休眠对执行节奏的影响。
多线程 08:实现 Callable 接口
8.1 核心优势
Callable 接口相比 Runnable 支持 有返回值 的线程任务,可抛出异常,是实现带返回值线程的标准方式(通常配合线程池使用)。
8.2 核心方法
Callable 接口仅定义一个方法:V call() throws Exception,线程任务逻辑写在 call() 中,返回值为任务执行结果。
8.3 实现步骤
- 定义类实现
Callable<V>接口,指定返回值类型。 - 重写
call()方法,编写带返回值的任务逻辑。 - 创建
FutureTask对象,包装Callable任务(FutureTask实现了Runnable和Future接口)。 - 将
FutureTask传入Thread构造器,启动线程。 - 调用
FutureTask.get()获取线程执行结果(会阻塞等待线程完成)。
8.4 代码示例
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 带返回值的线程任务:计算1~100的和
class SumCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
Thread.sleep(10); // 模拟耗时
}
return sum;
}
}
// 测试类
public class CallableDemo {
public static void main(String[] args) {
// 创建Callable任务
SumCallable sumTask = new SumCallable();
// 包装为FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(sumTask);
// 创建线程并启动
Thread thread = new Thread(futureTask, "计算线程");
thread.start();
try {
// 获取线程执行结果(阻塞等待)
int result = futureTask.get();
System.out.println("1~100的和为:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
8.5 关键特性
get()方法会阻塞当前线程,直到任务执行完成并返回结果。- 可通过
isDone()判断任务是否执行完成。 - 支持抛出异常,便于处理任务执行中的错误。
多线程 09:静态代理模式
9.1 什么是静态代理
静态代理是代理模式的基础实现,指在编译期就确定代理关系,由代理类完全代理真实对象的行为,在不修改真实对象代码的前提下,对方法进行增强。
9.2 核心角色
- 抽象角色:定义统一的接口(如
Runnable),约束真实对象和代理对象的行为。 - 真实角色:实现抽象接口,编写核心业务逻辑。
- 代理角色:实现抽象接口,持有真实对象的引用,在调用真实对象方法前后添加增强逻辑。
9.3 代码示例(结合线程场景)
// 1. 抽象角色:定义线程任务接口
interface Task {
void execute();
}
// 2. 真实角色:核心业务逻辑
class RealTask implements Task {
@Override
public void execute() {
System.out.println("执行核心业务:线程任务处理");
}
}
// 3. 代理角色:静态代理,增强真实对象的方法
class ProxyTask implements Task {
// 持有真实对象的引用
private final Task realTask;
public ProxyTask(Task realTask) {
this.realTask = realTask;
}
@Override
public void execute() {
// 前置增强:任务执行前的操作
System.out.println("【代理增强】任务开始前,初始化资源、记录日志");
// 调用真实对象的核心方法
realTask.execute();
// 后置增强:任务执行后的操作
System.out.println("【代理增强】任务结束后,释放资源、统计耗时");
}
}
// 测试类
public class StaticProxyDemo {
public static void main(String[] args) {
// 创建真实任务对象
Task realTask = new RealTask();
// 创建代理对象,包装真实任务
Task proxyTask = new ProxyTask(realTask);
// 执行代理方法(自动触发增强逻辑)
proxyTask.execute();
}
}
9.4 与线程的关联
Java 中Thread类本质就是静态代理的典型应用:
Runnable是抽象角色,定义run()方法;- 自定义任务类是真实角色,实现
run()的核心逻辑; Thread是代理角色,持有Runnable引用,在start()中调用run(),同时封装线程启动、调度的底层逻辑。
9.5 优缺点
✅ 优点:不修改真实对象代码,符合开闭原则,逻辑简单易实现。❌ 缺点:每个真实对象都需要对应一个代理类,类数量爆炸,维护成本高。
多线程 10:Lambda 表达式
10.1 什么是 Lambda 表达式
Lambda 表达式是 Java 8 引入的函数式编程特性,用于简化只有一个抽象方法的接口(函数式接口) 的实现,大幅减少冗余代码,在多线程中常用于简化Runnable、Callable的实现。
10.2 核心语法
(参数列表) -> { 方法体 }
- 简化规则:参数类型可省略、单参数可省略括号、单语句可省略大括号和
return。 - 前提:接口必须是函数式接口(仅含一个抽象方法,如
Runnable、Callable、Comparator)。
10.3 代码示例(线程场景)
传统方式(匿名内部类)
// 传统方式:匿名内部类实现Runnable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("传统方式:线程任务执行");
}
}).start();
Lambda 简化方式
// Lambda简化:一行代码实现线程任务
new Thread(() -> System.out.println("Lambda方式:线程任务执行")).start();
带参数的 Lambda 示例
// 函数式接口:自定义带参数的任务接口
@FunctionalInterface
interface CalcTask {
int calculate(int a, int b);
}
// Lambda实现接口
CalcTask add = (a, b) -> a + b;
System.out.println("1+2=" + add.calculate(1, 2)); // 输出3
10.4 多线程中的应用
Lambda 表达式完美适配多线程场景,可大幅简化线程创建代码:
// 多线程Lambda实战:同时启动3个线程
for (int i = 0; i < 3; i++) {
new Thread(() -> System.out.println(Thread.currentThread().getName() + "执行任务"))
.start();
}
10.5 注意事项
- Lambda 只能用于函数式接口,不能用于普通类或多方法接口。
- Lambda 中访问外部变量时,变量默认是
final(不可修改)。
多线程 11:线程停止
11.1 线程停止的核心原则
Java 中不推荐使用stop()、suspend()等过时方法强制停止线程(会导致资源泄漏、数据不一致),推荐使用标志位中断法,让线程安全、优雅地退出。
11.2 推荐实现方式:标志位中断
通过自定义volatile标志位,控制线程的执行与停止,保证线程在退出前完成资源清理。
代码示例
class StopThread implements Runnable {
// 1. 定义volatile标志位(保证多线程间的可见性)
private volatile boolean flag = true;
@Override
public void run() {
// 2. 循环判断标志位,控制线程执行
while (flag) {
System.out.println(Thread.currentThread().getName() + ":线程正在执行");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
// 捕获中断异常后,可选择退出线程
Thread.currentThread().interrupt(); // 重新设置中断状态
return;
}
}
// 3. 线程退出前的资源清理
System.out.println(Thread.currentThread().getName() + ":线程安全退出,资源已清理");
}
// 4. 对外提供停止方法,修改标志位
public void stopThread() {
this.flag = false;
}
}
// 测试类
public class ThreadStopDemo {
public static void main(String[] args) throws InterruptedException {
StopThread stopThread = new StopThread();
new Thread(stopThread, "线程A").start();
// 主线程休眠3秒后,停止线程A
Thread.sleep(3000);
stopThread.stopThread();
}
}
11.3 中断方法interrupt()的使用
interrupt()用于中断线程,但不会直接停止线程,仅会设置线程的中断状态:
- 若线程处于阻塞状态(
sleep()、wait()、join()),会抛出InterruptedException,并清除中断状态; - 若线程处于运行状态,仅设置中断标志位,需通过
isInterrupted()判断并手动退出。
代码示例
Thread thread = new Thread(() -> {
// 循环判断中断状态
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程执行中...");
}
System.out.println("线程因中断退出");
});
thread.start();
Thread.sleep(1000);
thread.interrupt(); // 发送中断信号
11.4 注意事项
- 绝对禁止使用
stop()方法,会强制终止线程,导致锁未释放、数据损坏等严重问题。 - 标志位必须用
volatile修饰,保证多线程间的可见性,避免出现死循环。
多线程 12:线程休眠_sleep
12.1 什么是sleep()
sleep(long millis)是Thread类的静态方法,用于让当前线程进入阻塞状态,暂停执行指定的毫秒数,时间结束后自动恢复到就绪状态,等待 CPU 调度。
12.2 核心特性
- 属于当前线程:
Thread.sleep()只会让调用该方法的线程休眠,不会影响其他线程。 - 不释放锁:线程休眠时,会持有已获取的锁,其他线程无法获取该锁。
- 可中断:休眠期间若调用
interrupt(),会抛出InterruptedException,并提前结束休眠。
12.3 代码示例
public class ThreadSleepDemo {
public static void main(String[] args) {
// 启动线程,模拟倒计时
new Thread(() -> {
for (int i = 10; i >= 0; i--) {
System.out.println("倒计时:" + i);
try {
// 线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("倒计时结束!");
}, "倒计时线程").start();
}
}
12.4 常见应用场景
- 模拟耗时任务,测试多线程并发效果;
- 实现定时任务、倒计时功能;
- 控制线程执行节奏,避免 CPU 空转。
12.5 注意事项
sleep()是静态方法,调用时会自动绑定当前线程,不要通过线程对象调用(如thread.sleep()无意义,实际休眠的是当前线程)。- 休眠时间结束后,线程不会立即执行,需等待 CPU 分配时间片。
多线程 13:线程礼让_yield
13.1 什么是yield()
yield()是Thread类的静态方法,用于让当前线程主动让出 CPU 执行权,从运行态回到就绪态,让同优先级或更高优先级的线程优先执行。
13.2 核心特性
- 礼让不保证:
yield()只是建议CPU 调度器让出执行权,调度器可能忽略该建议,线程可能立即再次获得 CPU 执行。 - 不释放锁:线程礼让时,不会释放已持有的锁。
- 仅礼让同优先级 / 高优先级线程:低优先级线程无法通过
yield()获得执行权。
13.3 代码示例
public class ThreadYieldDemo {
public static void main(String[] args) {
// 线程A:执行时主动礼让
Thread threadA = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
// 主动礼让CPU
Thread.yield();
}
}, "线程A");
// 线程B:正常执行
Thread threadB = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
}
}, "线程B");
// 同时启动两个线程
threadA.start();
threadB.start();
}
}
13.4 运行效果
线程 A 会主动礼让,线程 B 的执行机会会明显增加,但无法保证线程 A 完全礼让完再执行线程 B,因为yield()不具备强制性。
13.5 与sleep()的区别
| 特性 | yield() |
sleep() |
|---|---|---|
| 线程状态 | 从运行态→就绪态 | 从运行态→阻塞态 |
| 执行恢复 | 立即参与 CPU 调度 | 休眠时间结束后参与调度 |
| 锁持有 | 不释放锁 | 不释放锁 |
| 强制性 | 无,仅为建议 | 有,强制阻塞指定时间 |
多线程 14:线程强制执行_join
14.1 什么是join()
join()是Thread类的实例方法,用于让当前线程阻塞,等待调用join()的线程执行完成后,再继续执行,实现线程的 “插队” 效果。
14.2 核心方法
join():无限等待,直到目标线程执行完成;join(long millis):等待指定毫秒数,超时后自动恢复执行。
14.3 代码示例
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始执行");
// 创建子线程
Thread subThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程执行完成");
}, "子线程");
// 启动子线程
subThread.start();
// 子线程插队:主线程阻塞,等待子线程执行完成后再继续
subThread.join();
System.out.println("主线程继续执行,程序结束");
}
}
14.4 运行效果
主线程启动子线程后,会被join()阻塞,直到子线程完全执行完毕,主线程才会继续执行,实现 “子线程优先执行” 的效果。
14.5 常见应用场景
- 多线程任务中,需要等待某个子线程执行完成后,再执行后续逻辑(如数据加载完成后再渲染页面);
- 线程间的执行顺序控制,保证任务的依赖关系。
14.6 注意事项
join()会让当前线程(调用join()的线程) 阻塞,而非目标线程。join()会释放锁,阻塞期间其他线程可获取锁资源。
多线程 15:观测线程状态
15.1 线程的 6 种核心状态
Java 中线程的状态由Thread.State枚举定义,共 6 种状态,线程在生命周期中会在这些状态间切换:
| 状态 | 说明 |
|---|---|
| NEW(新建) | 线程已创建,但未调用start(),尚未启动 |
| RUNNABLE(可运行) | 线程正在执行,或等待 CPU 分配时间片(就绪态 + 运行态合并) |
| BLOCKED(阻塞) | 线程等待获取锁,被阻塞在同步块 / 方法前 |
| WAITING(无限等待) | 线程无限等待,需被其他线程唤醒(如wait()、join()) |
| TIMED_WAITING(计时等待) | 线程等待指定时间,时间到后自动唤醒(如sleep()、wait(long)) |
| TERMINATED(终止) | 线程执行完成,生命周期结束 |
15.2 观测线程状态的方法
通过Thread.getState()方法,可获取线程当前的状态,用于调试、监控线程执行流程。
15.3 代码示例
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 新建状态:线程创建后未启动
Thread thread = new Thread(() -> {
try {
// 3. 计时等待状态:线程休眠
Thread.sleep(1000);
// 线程执行完成,进入终止状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "测试线程");
// 观测新建状态
System.out.println("1. 新建状态:" + thread.getState()); // NEW
// 2. 启动线程,进入可运行状态
thread.start();
System.out.println("2. 启动后状态:" + thread.getState()); // RUNNABLE
// 3. 主线程休眠200ms,观测线程的计时等待状态
Thread.sleep(200);
System.out.println("3. 休眠中状态:" + thread.getState()); // TIMED_WAITING
// 4. 等待线程执行完成,观测终止状态
thread.join();
System.out.println("4. 执行完成状态:" + thread.getState()); // TERMINATED
}
}
15.4 状态切换流程
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → RUNNABLE → TERMINATED
15.5 注意事项
RUNNABLE包含了 “就绪” 和 “运行” 两种状态,Java 中不区分这两个状态。- 线程一旦进入
TERMINATED状态,无法再次启动。
多线程 16:线程的优先级
16.1 什么是线程优先级
线程优先级用于告诉 CPU 调度器,哪些线程更优先执行,优先级越高,获得 CPU 执行权的概率越大(不保证绝对优先,仅为概率性)。
16.2 优先级范围
Java 中线程优先级范围为1~10,Thread类定义了 3 个常量:
MIN_PRIORITY = 1:最低优先级NORM_PRIORITY = 5:默认优先级(所有线程默认优先级为 5)MAX_PRIORITY = 10:最高优先级
16.3 核心方法
setPriority(int newPriority):设置线程优先级;getPriority():获取线程当前优先级。
16.4 代码示例
public class ThreadPriorityDemo {
public static void main(String[] args) {
// 线程1:最高优先级
Thread highPriority = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
}
}, "高优先级线程");
highPriority.setPriority(Thread.MAX_PRIORITY);
// 线程2:最低优先级
Thread lowPriority = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
}
}, "低优先级线程");
lowPriority.setPriority(Thread.MIN_PRIORITY);
// 同时启动两个线程
lowPriority.start();
highPriority.start();
}
}
16.5 运行效果
高优先级线程获得 CPU 执行权的概率更高,执行次数会明显多于低优先级线程,但无法保证高优先级线程先执行完,因为 CPU 调度具有随机性。
16.6 注意事项
- 优先级不保证执行顺序:仅为概率性提升,不能依赖优先级控制线程执行顺序。
- 优先级继承:子线程的优先级默认继承父线程的优先级。
- 不要过度依赖优先级:优先级仅用于优化性能,不能作为业务逻辑的控制手段。
- 优先级设置需在
start()之前:启动线程后设置优先级无效。
多线程 17:守护线程
17.1 什么是守护线程
守护线程(Daemon Thread)是服务于用户线程的后台线程,核心特点是:当所有用户线程执行完毕后,守护线程会自动终止,不会阻止 JVM 退出。
17.2 核心特性
- 生命周期依赖用户线程:只要有一个用户线程在运行,守护线程就会运行;所有用户线程结束,守护线程立即终止。
- 优先级低:默认优先级低于用户线程,仅在 CPU 空闲时执行。
- 典型应用:垃圾回收线程(GC)、后台日志记录、JVM 监控线程、文件自动保存线程。
17.3 核心方法
setDaemon(boolean on):设置线程为守护线程(true)或用户线程(false),必须在start()之前调用。isDaemon():判断线程是否为守护线程。
17.4 代码示例
public class DaemonThreadDemo {
public static void main(String[] args) {
// 1. 创建守护线程:模拟后台日志记录
Thread daemonThread = new Thread(() -> {
int count = 0;
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【守护线程】后台日志记录中... " + count++);
}
}, "守护线程-日志");
// 2. 设置为守护线程(必须在start()前调用)
daemonThread.setDaemon(true);
// 启动守护线程
daemonThread.start();
// 3. 创建用户线程:模拟主业务
Thread userThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【用户线程】主业务执行中... " + i);
}
System.out.println("【用户线程】主业务执行完成");
}, "用户线程-主业务");
// 启动用户线程
userThread.start();
}
}
17.5 运行效果
用户线程执行 5 次后结束,JVM 中无活跃用户线程,守护线程会自动终止,程序退出。
17.6 注意事项
- 守护线程中不要执行读写文件、数据库操作等需要保证完成的任务,因为 JVM 退出时会直接终止守护线程,可能导致数据丢失。
- 守护线程创建的子线程默认也是守护线程。
多线程 18:线程同步机制
18.1 为什么需要线程同步
多线程并发操作共享资源时,会出现线程安全问题(如超卖、数据不一致),线程同步机制的核心作用是:保证同一时间只有一个线程操作共享资源,避免并发冲突。
18.2 核心原理
通过锁机制实现同步:线程操作共享资源前,必须先获取锁;获取锁后执行操作,操作完成后释放锁;其他线程需等待锁释放后才能获取锁并执行,保证操作的原子性、可见性、有序性。
18.3 同步的三大特性
- 原子性:操作不可分割,要么全部执行,要么全部不执行。
- 可见性:一个线程修改了共享变量,其他线程能立即看到最新值。
- 有序性:禁止指令重排序,保证代码按预期顺序执行。
18.4 常见同步实现方式
synchronized关键字(同步方法、同步块)Lock接口(ReentrantLock等)- 原子类(
AtomicInteger等) - 并发容器(
CopyOnWriteArrayList等)
多线程 19:三大不安全案例
19.1 案例 1:多线程卖票(超卖问题)
多个窗口同时卖票,未同步导致超卖、重复卖票。
class Ticket implements Runnable {
// 共享票源
private int ticket = 10;
@Override
public void run() {
while (true) {
if (ticket <= 0) break;
try {
// 模拟售票耗时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
ticket--;
}
}
}
public class UnsafeTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 3个窗口同时卖票
new Thread(ticket, "窗口1").start();
new Thread(ticket, "窗口2").start();
new Thread(ticket, "窗口3").start();
}
}
问题:出现同一张票被多个窗口售出、票号为负数的情况。
19.2 案例 2:多线程取钱(余额不一致)
多个账户同时取钱,未同步导致余额计算错误。
class Account {
// 账户余额
private int balance = 1000;
// 取钱方法
public void withdraw(int amount) {
if (balance >= amount) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取钱" + amount + ",余额:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "余额不足");
}
}
}
public class UnsafeAccountDemo {
public static void main(String[] args) {
Account account = new Account();
// 两个人同时取钱
new Thread(() -> account.withdraw(800), "小明").start();
new Thread(() -> account.withdraw(800), "小红").start();
}
}
问题:两人同时取钱,余额变为负数,出现透支。
19.3 案例 3:多线程操作集合(并发修改异常)
多线程同时操作ArrayList,导致ConcurrentModificationException。
public class UnsafeListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 1000个线程同时向集合添加元素
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
// 等待所有线程执行完成
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("集合大小:" + list.size());
}
}
问题:集合大小小于 1000,甚至抛出并发修改异常。
多线程 20:同步方法及同步块
20.1 synchronized关键字
synchronized是 Java 内置的同步机制,可修饰方法或代码块,自动加锁、释放锁,是最常用的线程安全实现方式。
20.2 同步方法
修饰实例方法,锁对象是当前对象(this);修饰静态方法,锁对象是类的 Class 对象。
代码示例:卖票问题修复
class Ticket implements Runnable {
private int ticket = 10;
// 同步方法:锁对象是this
@Override
public synchronized void run() {
while (true) {
if (ticket <= 0) break;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
ticket--;
}
}
}
20.3 同步块
synchronized(锁对象),可自定义锁对象,粒度更灵活,推荐使用。
- 锁对象可以是任意对象,但必须是共享对象(所有线程使用同一个锁)。
- 常用锁对象:
this(实例对象)、类.class(静态同步)、共享资源对象。
代码示例:取钱问题修复
class Account {
private int balance = 1000;
public void withdraw(int amount) {
// 同步块:锁对象为当前账户对象
synchronized (this) {
if (balance >= amount) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取钱" + amount + ",余额:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "余额不足");
}
}
}
}
20.4 同步方法 vs 同步块
| 特性 | 同步方法 | 同步块 |
|---|---|---|
| 锁对象 | 自动绑定this/类.class |
可自定义锁对象 |
| 粒度 | 粗粒度(整个方法) | 细粒度(仅同步核心代码) |
| 灵活性 | 低 | 高 |
| 性能 | 相对较低(锁范围大) | 相对较高(锁范围小) |
20.5 注意事项
- 锁对象必须唯一,否则无法实现同步。
synchronized是可重入锁,线程获取锁后可再次获取,不会死锁。- 同步会降低程序性能,尽量缩小同步范围。
多线程 21:CopyOnWriteArrayList
21.1 什么是CopyOnWriteArrayList
CopyOnWriteArrayList是 Java 并发包提供的线程安全的 List 实现,核心原理是写时复制:修改集合时,复制原集合的副本,在副本上修改,修改完成后替换原集合,读操作无锁,写操作加锁。
21.2 核心特性
- 读操作完全无锁:读性能极高,适合读多写少的场景。
- 写操作加锁 + 复制:写操作会加锁,复制原集合,避免读写冲突。
- 解决并发修改异常:读操作基于原集合,写操作基于副本,不会出现
ConcurrentModificationException。
21.3 代码示例:集合安全修复
import java.util.concurrent.CopyOnWriteArrayList;
public class SafeListDemo {
public static void main(String[] args) {
// 使用CopyOnWriteArrayList替代ArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("集合大小:" + list.size()); // 输出1000,无异常
}
}
21.4 适用场景
- 读多写少的场景:如缓存、配置列表、白名单列表。
- 不适合写频繁的场景:写操作需要复制数组,内存开销大,性能低。
21.5 与ArrayList的对比
| 特性 | ArrayList |
CopyOnWriteArrayList |
|---|---|---|
| 线程安全 | 不安全 | 安全 |
| 读性能 | 高 | 极高(无锁) |
| 写性能 | 高 | 低(复制 + 加锁) |
| 内存开销 | 低 | 高(写时复制) |
| 并发修改异常 | 会出现 | 不会出现 |
多线程 22:死锁
22.1 什么是死锁
死锁是指两个或多个线程互相持有对方需要的锁,同时等待对方释放锁,导致所有线程永久阻塞,程序无法继续执行的状态。
22.2 死锁产生的四大必要条件(缺一不可)
- 互斥条件:资源只能被一个线程持有,其他线程只能等待。
- 请求与保持条件:线程持有一个锁的同时,请求另一个锁,不释放已持有的锁。
- 不剥夺条件:线程已持有的锁不能被强制剥夺,只能主动释放。
- 循环等待条件:多个线程形成循环等待链,每个线程等待下一个线程持有的锁。
22.3 代码示例:死锁模拟
// 锁对象A
class LockA {}
// 锁对象B
class LockB {}
public class DeadLockDemo {
// 两个共享锁对象
static LockA lockA = new LockA();
static LockB lockB = new LockB();
public static void main(String[] args) {
// 线程1:先拿A锁,再拿B锁
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1持有A锁,等待B锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("线程1持有A、B锁");
}
}
}, "线程1").start();
// 线程2:先拿B锁,再拿A锁
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2持有B锁,等待A锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("线程2持有A、B锁");
}
}
}, "线程2").start();
}
}
运行效果:两个线程互相等待,永久阻塞,程序无法结束。
22.4 死锁的避免与解决
- 破坏四大条件:
- 破坏请求与保持:一次性申请所有锁,不分步申请。
- 破坏循环等待:按固定顺序获取锁(如统一先拿 A 再拿 B)。
- 破坏不剥夺:获取锁失败时,释放已持有的锁。
- 使用定时锁:
Lock.tryLock(long timeout),超时自动释放锁。 - 避免嵌套同步:尽量减少同步块嵌套,降低锁竞争。
多线程 23:Lock 锁
23.1 什么是 Lock 锁
Lock是 Java 5 提供的显式锁接口,相比synchronized隐式锁,提供了更灵活的锁控制,常用实现类是ReentrantLock(可重入锁)。
23.2 synchronized vs Lock
| 特性 | synchronized |
Lock |
|---|---|---|
| 锁类型 | 隐式锁(自动加锁、释放锁) | 显式锁(手动加锁、释放锁) |
| 可中断 | 不可中断 | 可中断(lockInterruptibly()) |
| 定时锁 | 无 | 有(tryLock(long timeout)) |
| 公平锁 | 非公平 | 支持公平 / 非公平 |
| 锁绑定 | 绑定线程 | 不绑定线程 |
| 性能 | JDK1.6 后优化,性能接近 Lock | 高并发下性能更优 |
23.3 核心方法
lock():加锁,阻塞直到获取锁。unlock():释放锁,必须在 finally 中执行,避免锁泄漏。tryLock():尝试获取锁,立即返回结果,不阻塞。lockInterruptibly():可中断加锁,线程可被中断。
23.4 代码示例:卖票问题 Lock 锁修复
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket implements Runnable {
private int ticket = 10;
// 创建可重入锁
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
if (ticket <= 0) break;
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁:必须在finally中,保证锁一定被释放
lock.unlock();
}
}
}
}
public class LockDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "窗口1").start();
new Thread(ticket, "窗口2").start();
new Thread(ticket, "窗口3").start();
}
}
23.5 注意事项
unlock()必须在finally中执行,否则程序异常时锁无法释放,导致死锁。ReentrantLock是可重入锁,支持同一线程多次获取锁。- 公平锁:按线程申请顺序分配锁,性能低;非公平锁:随机分配,性能高,默认非公平。
多线程 24:生产者消费者问题
24.1 什么是生产者消费者问题
生产者消费者是经典的多线程同步问题:生产者线程生产数据,消费者线程消费数据,生产者生产满后等待,消费者消费空后等待,通过同步机制协调两者的执行。
24.2 核心需求
- 生产者:缓冲区满时停止生产,等待消费者消费。
- 消费者:缓冲区空时停止消费,等待生产者生产。
- 保证线程安全,避免数据丢失、重复消费。
24.3 实现方式:synchronized + wait() + notify()
wait():线程释放锁,进入等待状态。notify():唤醒一个等待的线程。notifyAll():唤醒所有等待的线程。
24.4 代码示例:生产者消费者实现
// 缓冲区:存储数据
class Buffer {
private int data;
// 缓冲区是否有数据
private boolean hasData = false;
// 生产者生产数据
public synchronized void produce(int data) {
// 缓冲区有数据,生产者等待
while (hasData) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产数据
this.data = data;
hasData = true;
System.out.println("生产者生产:" + data);
// 唤醒消费者
notifyAll();
}
// 消费者消费数据
public synchronized int consume() {
// 缓冲区无数据,消费者等待
while (!hasData) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费数据
hasData = false;
System.out.println("消费者消费:" + data);
// 唤醒生产者
notifyAll();
return data;
}
}
// 生产者线程
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
buffer.produce(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
buffer.consume();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ProducerConsumerDemo {
public static void main(String[] args) {
Buffer buffer = new Buffer();
// 启动生产者和消费者线程
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
}
24.5 核心注意事项
wait()必须在synchronized块中调用,否则抛出IllegalMonitorStateException。- 使用
while循环判断等待条件,避免虚假唤醒(线程被唤醒后,条件仍不满足)。 notifyAll()优先于notify(),避免唤醒错误线程导致死锁。
多线程 25:管程法
25.1 什么是管程法
管程(Monitor)是一种同步机制的抽象,核心思想是:将共享资源和操作共享资源的方法封装在一个对象内部,同一时间只允许一个线程进入管程执行方法,其他线程阻塞等待。Java 中synchronized关键字的底层就是基于管程实现的,管程法是生产者消费者问题的经典解决方案之一。
25.2 核心原理
- 封装共享资源:将缓冲区、数据等共享资源封装在管程类中,对外仅暴露同步方法。
- 互斥访问:管程内的方法默认互斥,同一时间只有一个线程能执行,保证线程安全。
- 条件变量:通过
wait()/notify()实现线程的等待与唤醒,协调生产者和消费者的执行节奏。
25.3 代码示例:管程法实现生产者消费者
// 管程类:封装缓冲区和同步方法
class SynchroBuffer {
// 缓冲区:最多存储10个数据
private final Object[] buffer = new Object[10];
// 缓冲区当前元素数量
private int count = 0;
// 生产者生产方法(同步,互斥访问)
public synchronized void put(Object data) throws InterruptedException {
// 缓冲区满,生产者等待
while (count == buffer.length) {
wait();
}
// 生产数据:放入缓冲区
buffer[count] = data;
count++;
System.out.println("生产者生产:" + data + ",缓冲区剩余:" + (buffer.length - count));
// 唤醒消费者
notifyAll();
}
// 消费者消费方法(同步,互斥访问)
public synchronized Object get() throws InterruptedException {
// 缓冲区空,消费者等待
while (count == 0) {
wait();
}
// 消费数据:从缓冲区取出
count--;
Object data = buffer[count];
System.out.println("消费者消费:" + data + ",缓冲区剩余:" + count);
// 唤醒生产者
notifyAll();
return data;
}
}
// 生产者线程
class Producer implements Runnable {
private final SynchroBuffer buffer;
public Producer(SynchroBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
buffer.put(i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程
class Consumer implements Runnable {
private final SynchroBuffer buffer;
public Consumer(SynchroBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
buffer.get();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class MonitorDemo {
public static void main(String[] args) {
SynchroBuffer buffer = new SynchroBuffer();
// 启动生产者和消费者线程
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
}
25.4 核心优势
- 封装性强:将共享资源和同步逻辑封装在管程内部,对外隐藏实现细节,代码更易维护。
- 安全性高:自动保证互斥访问,避免手动加锁的失误。
- Java 原生支持:基于
synchronized实现,无需额外依赖,兼容性好。
多线程 26:信号灯法
26.1 什么是信号灯法
信号灯法(Semaphore)是另一种经典的同步机制,核心是通过信号量(计数器) 控制线程的执行:
- 信号量 > 0:线程可执行,执行后信号量 - 1(P 操作)
- 信号量 = 0:线程阻塞等待,其他线程执行后信号量 + 1(V 操作)Java 中
Semaphore类是信号灯法的标准实现,常用于控制并发线程数、生产者消费者问题。
26.2 核心原理
- 信号量初始化:设置初始许可数(如生产者消费者中,初始许可数为缓冲区大小)。
- P 操作(申请许可):线程执行前申请许可,许可不足则阻塞。
- V 操作(释放许可):线程执行完成后释放许可,唤醒等待线程。
26.3 代码示例:信号灯法实现生产者消费者
import java.util.concurrent.Semaphore;
// 缓冲区类
class Buffer {
private final Object[] buffer = new Object[10];
// 空缓冲区信号量:初始10个许可(可生产10个数据)
private final Semaphore empty = new Semaphore(10);
// 满缓冲区信号量:初始0个许可(无数据可消费)
private final Semaphore full = new Semaphore(0);
// 互斥锁:保证缓冲区操作的原子性
private final Semaphore mutex = new Semaphore(1);
private int in = 0; // 生产者写入位置
private int out = 0; // 消费者读取位置
// 生产者生产
public void put(Object data) throws InterruptedException {
// 申请空缓冲区许可
empty.acquire();
// 申请互斥锁
mutex.acquire();
// 写入数据
buffer[in] = data;
in = (in + 1) % buffer.length;
System.out.println("生产者生产:" + data);
// 释放互斥锁
mutex.release();
// 释放满缓冲区许可(通知消费者有数据可消费)
full.release();
}
// 消费者消费
public Object get() throws InterruptedException {
// 申请满缓冲区许可
full.acquire();
// 申请互斥锁
mutex.acquire();
// 读取数据
Object data = buffer[out];
out = (out + 1) % buffer.length;
System.out.println("消费者消费:" + data);
// 释放互斥锁
mutex.release();
// 释放空缓冲区许可(通知生产者有空间可生产)
empty.release();
return data;
}
}
// 生产者线程
class Producer implements Runnable {
private final Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
buffer.put(i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程
class Consumer implements Runnable {
private final Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
buffer.get();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class SemaphoreDemo {
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
}
26.4 核心应用场景
- 控制并发线程数:如接口限流,限制同时访问的线程数。
- 生产者消费者问题:通过信号量协调生产和消费的节奏。
- 资源池管理:如数据库连接池,控制同时使用的连接数。
26.5 管程法 vs 信号灯法
| 特性 | 管程法(synchronized) | 信号灯法(Semaphore) |
|---|---|---|
| 实现方式 | 基于对象锁,封装同步逻辑 | 基于信号量计数器,灵活控制 |
| 并发控制 | 同一时间仅一个线程访问 | 可控制多个线程同时访问 |
| 适用场景 | 简单同步、生产者消费者 | 复杂并发控制、限流、资源池 |
| 灵活性 | 低 | 高 |
多线程 27:线程池
27.1 什么是线程池
线程池是管理线程的容器,核心思想是提前创建一批线程,放入池中复用,避免频繁创建 / 销毁线程的开销,同时控制线程的最大并发数,提升程序性能和稳定性。
27.2 线程池的核心优势
- 降低资源消耗:复用线程,避免频繁创建 / 销毁线程的开销。
- 提高响应速度:任务到达时,直接使用池中的线程执行,无需等待创建。
- 提高线程可管理性:统一管理线程,避免无限制创建线程导致的系统崩溃。
27.3 线程池的核心参数(ThreadPoolExecutor)
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:常驻线程,即使空闲也不会销毁
int maximumPoolSize, // 最大线程数:线程池允许的最大线程数
long keepAliveTime, // 空闲线程存活时间:非核心线程空闲后销毁的时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列:存储等待执行的任务
ThreadFactory threadFactory, // 线程工厂:创建线程
RejectedExecutionHandler handler // 拒绝策略:任务队列满、线程数满时的处理策略
)
27.4 4 种拒绝策略
| 策略 | 说明 |
|---|---|
AbortPolicy |
默认策略,直接抛出异常,拒绝任务 |
CallerRunsPolicy |
由调用线程(主线程)执行任务 |
DiscardPolicy |
直接丢弃任务,不抛出异常 |
DiscardOldestPolicy |
丢弃队列中最老的任务,执行新任务 |
27.5 常用线程池工具类(Executors)
Java 提供Executors工具类,快速创建常用线程池:
newFixedThreadPool(int nThreads):固定大小线程池,核心线程数 = 最大线程数newCachedThreadPool():缓存线程池,线程数无上限,空闲线程 60 秒后销毁newSingleThreadExecutor():单线程线程池,保证任务顺序执行newScheduledThreadPool(int corePoolSize):定时任务线程池,支持定时 / 周期执行
27.6 代码示例:线程池使用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小线程池(3个线程)
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2. 提交10个任务到线程池
for (int i = 1; i <= 10; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务:" + taskId);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池(不再接受新任务,等待现有任务执行完成)
pool.shutdown();
}
}
27.7 自定义线程池(推荐)
Executors创建的线程池存在 OOM 风险(如newCachedThreadPool无上限),生产环境推荐自定义线程池:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
// 自定义线程工厂:自定义线程名称,方便排查问题
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "自定义线程-" + count.getAndIncrement());
}
};
// 自定义线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 任务队列,容量10
threadFactory, // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 1; i <= 15; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务:" + taskId);
});
}
pool.shutdown();
}
}
27.8 注意事项
- 生产环境禁止使用
Executors创建线程池,推荐自定义ThreadPoolExecutor,避免 OOM。 - 线程池使用完成后,必须调用
shutdown()关闭,否则程序无法退出。 - 合理设置核心线程数:CPU 密集型任务设置为
CPU核心数+1,IO 密集型任务设置为2*CPU核心数。
多线程 28:总结
28.1 全 28 章知识体系梳理
一、基础概念(01-02)
- 进程、线程、多线程的核心定义与区别
- 多线程的执行原理与应用场景
二、线程创建(03-08)
- 3 种线程创建方式:继承
Thread类、实现Runnable接口、实现Callable接口 - 实战案例:网图下载、龟兔赛跑
三、线程控制(09-16)
- 静态代理模式、Lambda 表达式简化线程创建
- 线程生命周期控制:停止、休眠、礼让、强制执行
- 线程状态观测、线程优先级、守护线程
四、线程安全与同步(17-24)
- 线程同步机制、三大不安全案例
- 同步方法 / 同步块、
CopyOnWriteArrayList并发容器 - 死锁、
Lock锁、生产者消费者问题
五、高级同步与线程池(25-27)
- 管程法、信号灯法两种同步方案
- 线程池的原理、参数、自定义实现
28.2 核心知识点总结
- 线程创建:推荐实现
Runnable接口,避免单继承限制,支持资源共享。 - 线程安全:多线程操作共享资源必须加锁,常用
synchronized和ReentrantLock。 - 线程同步:管程法(synchronized)适合简单场景,信号灯法(Semaphore)适合复杂并发控制。
- 线程池:生产环境必须自定义线程池,避免
Executors的 OOM 风险,合理设置核心参数。 - 死锁避免:破坏四大必要条件,按固定顺序获取锁,避免嵌套同步。
- 守护线程:依赖用户线程,用户线程结束后自动终止,适合后台任务。
28.3 多线程最佳实践
- 避免共享资源:尽量使用局部变量,减少共享资源的竞争。
- 缩小同步范围:同步块粒度越小越好,避免整个方法同步,提升性能。
- 使用并发容器:优先使用
CopyOnWriteArrayList、ConcurrentHashMap等并发容器,替代普通集合。 - 合理使用线程池:复用线程,避免频繁创建 / 销毁,控制并发数。
- 避免死锁:统一锁的获取顺序,使用定时锁,减少嵌套同步。
- 优雅停止线程:使用标志位中断法,禁止使用
stop()等过时方法。
28.4 学习路线建议
- 掌握基础概念:进程、线程、多线程原理
- 熟练线程创建与控制:3 种创建方式、线程生命周期方法
- 攻克线程安全:同步机制、锁、死锁、生产者消费者
- 学习高级特性:信号灯法、线程池、并发容器
- 实战项目:多线程下载、并发接口、定时任务等
更多推荐




所有评论(0)