Java 多线程编程:从基础原理到实战应用
系统讲解 Java 多线程编程,涵盖进程与线程区别、三种线程创建方式(继承 Thread、实现 Runnable、实现 Callable)、线程生命周期状态转换。重点阐述并发安全问题的产生及解决方案,包括 synchronized 同步代码块/方法、Lock 锁机制。详细介绍线程池核心参数、创建方式及拒绝策略,并通过多线程文件下载实战案例演示线程池与同步机制的综合应用,旨在帮助开发者掌握高并发场景下的资源管理与性能优化技巧。

系统讲解 Java 多线程编程,涵盖进程与线程区别、三种线程创建方式(继承 Thread、实现 Runnable、实现 Callable)、线程生命周期状态转换。重点阐述并发安全问题的产生及解决方案,包括 synchronized 同步代码块/方法、Lock 锁机制。详细介绍线程池核心参数、创建方式及拒绝策略,并通过多线程文件下载实战案例演示线程池与同步机制的综合应用,旨在帮助开发者掌握高并发场景下的资源管理与性能优化技巧。

💡 掌握线程的核心概念,理解进程与线程的区别和联系。 💡 熟练掌握线程的三种创建方式,理解线程的生命周期及状态转换。 💡 掌握线程同步与锁机制,解决多线程并发安全问题。 💡 了解线程池的核心原理与使用方法,提升多线程程序性能。 ⚠️ 本章重点是 线程同步机制 和 线程池的实战应用,这是多线程开发中的核心难点和高频考点。
💡 进程是操作系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间和系统资源。比如打开一个 Java 程序,就会启动一个进程。 💡 线程是进程的执行单元,是 CPU 调度和执行的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 拥有独立的内存空间和资源 | 共享所属进程的内存和资源 |
| 开销成本 | 创建和销毁开销大 | 创建和销毁开销小 |
| 调度方式 | 由操作系统内核调度 | 由进程内部调度 |
| 独立性 | 进程之间相互独立 | 线程之间共享资源,依赖性强 |
✅ 核心结论:线程是轻量级的进程,多线程编程可以充分利用 CPU 资源,提升程序的并发执行效率。
在单线程程序中,代码是串行执行的,当遇到耗时操作(如文件读写、网络请求)时,程序会阻塞等待,造成 CPU 资源浪费。 多线程的优势体现在以下场景:
① 📝 定义一个类继承 Thread 类。
② 📝 重写 run() 方法,编写线程执行的业务逻辑。
③ 📝 创建线程对象,调用 start() 方法启动线程。
/**
* 继承 Thread 类创建线程
*/
public class MyThread extends Thread {
// 线程名称
private String threadName;
public MyThread(String threadName) {
this.threadName = threadName;
}
// 重写 run 方法,定义线程执行逻辑
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " 执行:" + i);
// 模拟线程执行耗时操作
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建线程对象
MyThread thread1 = new MyThread("线程 A");
MyThread thread2 = new MyThread("线程 B");
// 启动线程:注意是调用 start() 方法,不是 run() 方法
thread1.start();
thread2.start();
}
}
输出结果(顺序不固定,体现线程并发执行)
线程 A 执行:1 线程 B 执行:1 线程 A 执行:2 线程 B 执行:2 线程 A 执行:3 线程 B 执行:3 线程 A 执行:4 线程 B 执行:4 线程 A 执行:5 线程 B 执行:5
⚠️ 注意事项:启动线程必须调用 start() 方法,直接调用 run() 方法会变成普通方法的串行执行。
① 📝 定义一个类实现 Runnable 接口。
② 📝 重写 run() 方法,编写线程执行逻辑。
③ 📝 创建 Runnable 实现类对象,作为 Thread 构造方法的参数。
④ 📝 调用 start() 方法启动线程。
/**
* 实现 Runnable 接口创建线程
*/
public class MyRunnable implements Runnable {
private String taskName;
public MyRunnable(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " 执行:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建任务对象
MyRunnable task1 = new MyRunnable("任务 A");
MyRunnable task2 = new MyRunnable("任务 B");
// 创建线程对象,传入任务
Thread thread1 = new Thread(task1);
Thread thread2 (task2);
thread1.start();
thread2.start();
}
}
Thread 类后无法再继承其他类,而实现 Runnable 接口可以。Runnable 任务对象,适合多线程处理同一份资源的场景。/**
* 多线程共享资源:模拟售票系统
*/
public class TicketRunnable implements Runnable {
// 共享资源:10 张票
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
// 模拟售票耗时
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketNum-- + " 张票");
}
}
public static void main(String[] args) {
// 一个任务对象,多个线程共享
TicketRunnable ticketTask = new TicketRunnable();
// 三个线程模拟三个售票窗口
Thread window1 = new Thread(ticketTask, "窗口 1");
Thread window2 = new Thread(ticketTask, "窗口 2");
Thread window3 (ticketTask, );
window1.start();
window2.start();
window3.start();
}
}
输出结果(存在线程安全问题,后面会解决)
窗口 1 售出第 10 张票 窗口 2 售出第 9 张票 窗口 3 售出第 8 张票 窗口 1 售出第 7 张票 窗口 2 售出第 6 张票 窗口 3 售出第 5 张票 窗口 1 售出第 4 张票 窗口 2 售出第 3 张票 窗口 3 售出第 2 张票 窗口 1 售出第 1 张票
① 📝 定义一个类实现 Callable<V> 接口,V 是返回值类型。
② 📝 重写 call() 方法,编写线程执行逻辑,该方法可以抛出异常。
③ 📝 创建 Callable 实现类对象,通过 FutureTask 包装。
④ 📝 将 FutureTask 作为 Thread 构造方法参数,启动线程。
⑤ 📝 调用 FutureTask 的 get() 方法获取返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 实现 Callable 接口创建线程(带返回值)
*/
public class MyCallable implements Callable<Integer> {
// 计算 1 到 n 的累加和
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
System.out.println("计算中:" + i + ",当前和为:" + sum);
Thread.sleep(200);
}
return sum;
}
public static void main(String[] args) throws Exception {
// 创建 Callable 任务
MyCallable callableTask = new MyCallable(10);
// 用 FutureTask 包装,用于获取返回值
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
(futureTask);
thread.start();
futureTask.get();
System.out.println( + result);
}
}
输出结果
计算中:1,当前和为:1 计算中:2,当前和为:3 计算中:3,当前和为:6 计算中:4,当前和为:10 计算中:5,当前和为:15 计算中:6,当前和为:21 计算中:7,当前和为:28 计算中:8,当前和为:36 计算中:9,当前和为:45 计算中:10,当前和为:55 1 到 10 的累加和为:55
✅ 核心结论:三种创建方式对比
| 创建方式 | 优点 | 缺点 |
|---|---|---|
| 继承 Thread | 编程简单,直接使用 this 获取线程 | 单继承局限,无法共享资源 |
| 实现 Runnable | 避免单继承,支持资源共享 | 无返回值,无法抛出受检异常 |
| 实现 Callable | 有返回值,支持异常抛出 | 编程相对复杂 |
Java 中线程的生命周期包含六种状态,定义在 Thread.State 枚举中:
start() 方法。start() 方法后,线程处于就绪或运行中。
run() 方法。wait()、join() 等方法后进入,需要被其他线程唤醒。sleep(long)、wait(long) 等方法后进入,超时自动唤醒。NEW → RUNNABLE:调用 start() 方法
RUNNABLE → BLOCKED:竞争锁失败
BLOCKED → RUNNABLE:获取到锁资源
RUNNABLE → WAITING:调用 wait()/join() 方法
WAITING → RUNNABLE:其他线程调用 notify()/notifyAll() 方法
RUNNABLE → TIMED_WAITING:调用 sleep(long)/wait(long) 方法
TIMED_WAITING → RUNNABLE:超时自动唤醒或被其他线程唤醒
RUNNABLE → TERMINATED:run() 方法执行完毕或异常终止
| 方法 | 作用 | 注意事项 |
|---|---|---|
start() | 启动线程 | 一个线程只能调用一次 |
sleep(long millis) | 让线程休眠指定时间 | 不会释放持有的锁资源 |
wait() | 让线程进入等待状态 | 必须在同步代码块中调用,会释放锁 |
notify() | 唤醒一个等待的线程 | 必须在同步代码块中调用 |
notifyAll() | 唤醒所有等待的线程 | 必须在同步代码块中调用 |
join() | 等待该线程执行完毕 | 可以实现线程的顺序执行 |
/**
* 使用 join 方法实现线程顺序执行
*/
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("线程 A 执行:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("线程 B 执行:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程 A
threadA.start();
// 等待线程 A 执行完毕,再启动线程 B
threadA.join();
threadB.start();
}
}
输出结果(线程 A 执行完才会执行线程 B)
线程 A 执行:1 线程 A 执行:2 线程 A 执行:3 线程 B 执行:1 线程 B 执行:2 线程 B 执行:3
当多个线程同时操作共享资源时,会导致数据不一致的问题。比如前面的售票案例,在极端情况下会出现超卖或重复售票的问题。
问题复现:多线程售票的并发安全问题
public class UnsafeTicketDemo implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
// 模拟网络延迟,放大并发问题
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 核心问题:判断和操作不是原子性的
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketNum-- + " 张票");
}
}
public static void main(String[] args) {
UnsafeTicketDemo task = new UnsafeTicketDemo();
new Thread(task, "窗口 1").start();
new Thread(task, "窗口 2").start();
new Thread(task, "窗口 3").start();
}
}
可能出现的错误结果
窗口 1 售出第 10 张票 窗口 2 售出第 10 张票 窗口 3 售出第 9 张票 窗口 1 售出第 8 张票 窗口 2 售出第 7 张票 窗口 3 售出第 6 张票 窗口 1 售出第 5 张票 窗口 2 售出第 4 张票 窗口 3 售出第 3 张票 窗口 1 售出第 2 张票 窗口 2 售出第 1 张票 窗口 3 售出第 0 张票
可以看到出现了重复售票和售出 0 号票的问题,这就是典型的并发安全问题。
💡 同步代码块的核心是锁机制,通过 synchronized 关键字锁定一个对象,保证同一时间只有一个线程能执行代码块中的内容。
synchronized (锁对象) {
// 需要同步的代码(操作共享资源的代码)
}
public class SafeTicketDemo1 implements Runnable {
private int ticketNum = 10;
// 定义锁对象:必须是多个线程共享的对象
private final Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketNum-- + " 张票");
} else {
break;
}
}
}
}
public static void main(String[] args) {
SafeTicketDemo1 task = new SafeTicketDemo1();
new Thread(task, "窗口 1").start();
new (task, ).start();
(task, ).start();
}
}
输出结果(无重复、无超卖,解决并发安全问题)
窗口 1 售出第 10 张票 窗口 1 售出第 9 张票 窗口 2 售出第 8 张票 窗口 2 售出第 7 张票 窗口 3 售出第 6 张票 窗口 3 售出第 5 张票 窗口 1 售出第 4 张票 窗口 1 售出第 3 张票 窗口 2 售出第 2 张票 窗口 2 售出第 1 张票
⚠️ 注意事项:锁对象必须是多个线程共享的对象,否则无法实现同步效果。
💡 同步方法是将 synchronized 关键字修饰在方法上,等价于锁定当前对象(this)。
public synchronized 返回值类型 方法名 (参数列表) {
// 需要同步的代码
}
public class SafeTicketDemo2 implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (true) {
if (!sellTicket()) {
break;
}
}
}
// 同步方法:锁定的是 this 对象
private synchronized boolean sellTicket() {
if (ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketNum-- + " 张票");
return true;
} else {
return false;
}
}
public static void main(String[] args) {
SafeTicketDemo2 task = new SafeTicketDemo2();
new Thread(task, "窗口 1").start();
(task, ).start();
(task, ).start();
}
}
💡 java.util.concurrent.locks.Lock 是 JDK 5 新增的锁机制,相比 synchronized 更加灵活,可以手动控制锁的获取和释放。
常用实现类是 ReentrantLock(可重入锁)。
① 📝 创建 ReentrantLock 对象。
② 📝 在需要同步的代码前调用 lock() 方法获取锁。
③ 📝 在 finally 块中调用 unlock() 方法释放锁,确保锁一定会被释放。
import java.util.concurrent.locks.ReentrantLock;
public class SafeTicketDemo3 implements Runnable {
private int ticketNum = 10;
// 创建可重入锁对象
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 获取锁
lock.lock();
try {
if (ticketNum > 0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketNum-- + " 张票");
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁:必须放在 finally 块中
lock.unlock();
}
}
}
public static void main(String[] args) {
SafeTicketDemo3 task = new SafeTicketDemo3();
(task, ).start();
(task, ).start();
(task, ).start();
}
}
✅ 核心结论:synchronized 和 Lock 的对比
| 特性 | synchronized | Lock |
|---|---|---|
| 锁获取释放 | 自动获取和释放 | 手动调用 lock()/unlock() |
| 灵活性 | 低 | 高(支持公平锁/非公平锁) |
| 可中断性 | 不支持 | 支持 |
| 条件变量 | 不支持 | 支持(Condition) |
频繁创建和销毁线程会消耗大量系统资源,线程池可以复用线程,降低系统开销。线程池的核心优势:
Java 中线程池的核心实现类是 ThreadPoolExecutor,其构造方法包含 7 个核心参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// ...
}
Executors 提供了几种常用的线程池创建方法:
newFixedThreadPool(int nThreads):固定大小的线程池。newCachedThreadPool():缓存线程池,线程数可动态调整。newSingleThreadExecutor():单线程池,保证任务顺序执行。import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 固定大小线程池的使用
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小的线程池,核心线程数=最大线程数=3
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 2. 提交 10 个任务
for (int i = 1; i <= 10; i++) {
int taskId = i;
executorService.submit(() -> {
System.out.println("任务" + taskId + " 由线程 " + Thread.currentThread().getName() + " 执行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池
executorService.shutdown();
}
}
输出结果(3 个线程复用执行 10 个任务)
任务 1 由线程 pool-1-thread-1 执行 任务 2 由线程 pool-1-thread-2 执行 任务 3 由线程 pool-1-thread-3 执行 任务 4 由线程 pool-1-thread-1 执行 任务 5 由线程 pool-1-thread-2 执行 任务 6 由线程 pool-1-thread-3 执行 任务 7 由线程 pool-1-thread-1 执行 任务 8 由线程 pool-1-thread-2 执行 任务 9 由线程 pool-1-thread-3 执行 任务 10 由线程 pool-1-thread-1 执行
import java.util.concurrent.*;
/**
* 手动创建线程池(推荐方式)
*/
public class CustomThreadPoolDemo {
public static void main(String[] args) {
// 1. 定义线程池参数
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列:容量为 3 的阻塞队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(3);
// 线程工厂:默认线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略:丢弃任务并抛出异常
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 2. 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
// 3. 提交任务
for (int i ; i <= ; i++) {
i;
{
executor.submit(() -> {
System.out.println( + taskId + + Thread.currentThread().getName() + );
Thread.sleep();
});
} (Exception e) {
System.out.println( + taskId + + e.getMessage());
}
}
executor.shutdown();
}
}
输出结果(当任务数超过 最大线程数 + 队列容量=8 时,新任务被拒绝)
任务 1 由线程 pool-1-thread-1 执行 任务 2 由线程 pool-1-thread-2 执行 任务 3 由线程 pool-1-thread-3 执行 任务 4 由线程 pool-1-thread-4 执行 任务 5 由线程 pool-1-thread-5 执行 任务 6 由线程 pool-1-thread-1 执行 任务 7 由线程 pool-1-thread-2 执行 任务 8 由线程 pool-1-thread-3 执行 任务 9 被拒绝:Task java.util.concurrent.FutureTask@6d311334 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] 任务 10 被拒绝:Task java.util.concurrent.FutureTask@3d075dc0 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
⚠️ 注意事项:实际开发中推荐手动创建线程池,避免使用 Executors 工具类的默认方法,防止资源耗尽。
💡 实现一个多线程文件下载工具,支持从指定 URL 列表批量下载文件,要求:
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* 多线程文件批量下载工具
*/
public class MultiThreadFileDownloader {
// 线程池:核心线程数 2,最大线程数 4
private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
// 下载状态列表
private static final List<String> DOWNLOAD_STATUS = new ArrayList<>();
/**
* 下载单个文件
*
* @param fileUrl 文件 URL
* @param savePath 保存路径
*/
private static void downloadFile(String fileUrl, String savePath) {
HttpURLConnection connection = null;
InputStream in = null;
OutputStream ;
{
(fileUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout();
connection.setReadTimeout();
fileUrl.substring(fileUrl.lastIndexOf() + );
(savePath + File.separator + fileName);
in = connection.getInputStream();
out = (saveFile);
[] buffer = [ * ];
len;
((len = in.read(buffer)) != -) {
out.write(buffer, , len);
}
+ fileName;
(DOWNLOAD_STATUS) {
DOWNLOAD_STATUS.add(successMsg);
}
System.out.println(successMsg);
} (Exception e) {
fileUrl.substring(fileUrl.lastIndexOf() + );
+ fileName + + e.getMessage();
(DOWNLOAD_STATUS) {
DOWNLOAD_STATUS.add(failMsg);
}
System.out.println(failMsg);
} {
{
(out != ) out.close();
(in != ) in.close();
(connection != ) connection.disconnect();
} (IOException e) {
e.printStackTrace();
}
}
}
{
(savePath);
(!saveDir.exists()) {
saveDir.mkdirs();
}
(String url : urlList) {
EXECUTOR.submit(() -> downloadFile(url, savePath));
}
EXECUTOR.shutdown();
{
EXECUTOR.awaitTermination(, TimeUnit.HOURS);
} (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
(String status : DOWNLOAD_STATUS) {
System.out.println(status);
}
}
{
List<String> urlList = <>();
urlList.add();
urlList.add();
urlList.add();
urlList.add();
urlList.add();
batchDownload(urlList, );
}
}
✅ 这个多线程下载工具综合运用了线程池、同步机制、IO 流等知识,核心亮点:
synchronized 保证下载状态列表的线程安全。CallerRunsPolicy 拒绝策略,当任务过多时,由调用线程执行,避免任务丢失。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online