《Java 多线程全面解析:从基础到生产者消费者模型》
目录
一、多线程基础认知
1.1 什么是多线程?
1.2 并发与并行的区别
1.3 进程与线程的关系
二、Java 多线程的三种实现方式
2.1 方式一:继承 Thread 类
实现步骤
代码演示
2.2 方式二:实现 Runnable 接口
实现步骤
代码演示
2.3 方式三:实现 Callable 接口(JDK5+)
核心方法与类
实现步骤
代码演示
2.4 三种方式对比
三、线程常用操作
3.1 设置与获取线程名称
示例
3.2 线程休眠(sleep ())
示例
3.3 线程优先级(Priority)
示例
3.4 守护线程(Daemon Thread)
示例
四、线程同步:解决数据安全问题
4.1 数据安全问题的条件
4.2 同步代码块
语法
卖票案例优化(同步代码块)
4.3 同步方法
语法
4.4 Lock 锁(JDK5+)
核心方法
卖票案例优化(Lock 锁)
4.5 死锁(Deadlock)
死锁示例
避免死锁的原则
五、经典案例:生产者消费者模型
5.1 基础模型:基于 wait ()/notify ()
案例实现(汉堡包生产与消费)
在现代软件开发中,多线程是提升程序性能、优化资源利用率的核心技术之一。本文将从多线程基础概念出发,详细讲解 Java 中多线程的实现方式、线程同步机制,并通过经典的生产者消费者案例加深理解,帮助读者系统掌握多线程编程。
一、多线程基础认知
1.1 什么是多线程?
多线程是指从软件或硬件层面实现多个线程并发执行的技术。支持多线程的计算机通过硬件(如多核 CPU)或软件调度,让多个线程 “同时” 运行,从而提升程序处理效率(例如同时处理网络请求、文件读写等任务)。
1.2 并发与并行的区别
很多人会混淆 “并发” 和 “并行”,二者核心差异在于是否真正同时执行:
- 并行(Parallel):同一时刻,多个指令在多个 CPU上同时执行(如多核 CPU 同时处理两个线程的任务)。
- 并发(Concurrent):同一时刻,多个指令在单个 CPU上交替执行(CPU 通过快速切换线程,造成 “同时运行” 的错觉)。
1.3 进程与线程的关系
进程和线程是操作系统中两个核心概念,二者的关系可概括为 “进程包含线程,线程是进程的执行单元”:
特性 | 进程(Process) | 线程(Thread) |
---|---|---|
定义 | 正在运行的程序(资源分配的基本单位) | 进程中的单个顺序控制流(执行的基本单位) |
资源占用 | 独立占用内存、文件句柄等系统资源 | 共享所属进程的资源 |
独立性 | 进程间相互独立,一个崩溃不影响其他进程 | 线程依赖进程,一个线程崩溃可能导致进程崩溃 |
切换开销 | 切换成本高(需保存完整上下文) | 切换成本低(共享进程资源) |
- 单线程程序:一个进程只有一条执行路径(如早期的 Java 程序,main 方法就是单线程)。
- 多线程程序:一个进程有多条执行路径(如浏览器同时渲染页面、下载文件)。
二、Java 多线程的三种实现方式
Java 提供了三种主流的多线程实现方式,各有优缺点,适用于不同场景。
2.1 方式一:继承 Thread 类
Thread 类是 Java 中线程的基础类,通过继承它并重写run()
方法即可实现多线程。
实现步骤
- 定义类继承
Thread
; - 重写
run()
方法(封装线程执行的逻辑); - 创建线程对象,调用
start()
方法启动线程(注意:不能直接调用run()
,否则会作为普通方法执行)。
代码演示
// 自定义线程类
public class MyThread extends Thread {@Overridepublic void run() {// 线程执行逻辑:打印0-99for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}
}// 测试类
public class ThreadDemo {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程1");t2.setName("线程2");// 启动线程,JVM会自动调用run()t1.start();t2.start();}
}
2.2 方式二:实现 Runnable 接口
由于 Java 是单继承机制,继承Thread
会限制类的扩展性,因此推荐使用实现 Runnable 接口的方式。
实现步骤
- 定义类实现
Runnable
接口; - 重写
run()
方法; - 创建
Runnable
实现类对象,作为参数传入Thread
构造器; - 调用
Thread
对象的start()
方法启动线程。
代码演示
// 实现Runnable接口
public class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}
}// 测试类
public class RunnableDemo {public static void main(String[] args) {MyRunnable runnable = new MyRunnable();// 传入Runnable对象并指定线程名Thread t1 = new Thread(runnable, "窗口A");Thread t2 = new Thread(runnable, "窗口B");t1.start();t2.start();}
}
2.3 方式三:实现 Callable 接口(JDK5+)
前两种方式的run()
方法没有返回值且无法抛出受检异常,Callable
接口解决了这个问题,支持线程执行后返回结果。
核心方法与类
Callable<V>
:泛型接口,call()
方法返回泛型类型 V,可抛出异常;FutureTask<V>
:实现Future
接口,用于接收call()
的返回值,同时可作为Thread
的构造参数。
实现步骤
- 定义类实现
Callable<V>
接口,重写call()
方法; - 创建
Callable
实现类对象; - 创建
FutureTask
对象,传入Callable
对象; - 创建
Thread
对象,传入FutureTask
,调用start()
; - 调用
FutureTask
的get()
方法获取线程执行结果(该方法会阻塞,直到线程执行完成)。
代码演示
// 实现Callable接口,指定返回值类型为String
public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 100; i++) {System.out.println("执行任务:" + i);}return "任务执行完成!";}
}// 测试类
public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable callable = new MyCallable();FutureTask<String> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();// 获取返回结果(会阻塞直到线程结束)String result = futureTask.get();System.out.println("线程返回结果:" + result);}
}
2.4 三种方式对比
实现方式 | 优点 | 缺点 |
---|---|---|
继承 Thread 类 | 编程简单,可直接调用 Thread 的方法(如 getName ()) | 单继承限制,扩展性差 |
实现 Runnable 接口 | 无继承限制,扩展性强,支持多个线程共享资源 | 需通过Thread.currentThread() 获取线程对象 |
实现 Callable 接口 | 支持返回值和异常抛出,功能最完善 | 编程较复杂,需配合 FutureTask 使用 |
三、线程常用操作
3.1 设置与获取线程名称
线程名称用于标识线程,默认名称为Thread-0
、Thread-1
等,可通过以下方法自定义:
void setName(String name)
:设置线程名称;String getName()
:获取线程名称;static Thread currentThread()
:获取当前正在执行的线程对象。
示例
public class ThreadNameDemo {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println(Thread.currentThread().getName()); // 输出"自定义线程"});t.setName("自定义线程");t.start();System.out.println(Thread.currentThread().getName()); // 输出"main"(主线程名称)}
}
3.2 线程休眠(sleep ())
static void sleep(long millis)
方法让当前线程暂停执行指定毫秒数,常用于模拟延迟(如网络请求等待)。
示例
public class SleepDemo {public static void main(String[] args) throws InterruptedException {System.out.println("开始休眠");Thread.sleep(3000); // 休眠3秒System.out.println("休眠结束");}
}
3.3 线程优先级(Priority)
Java 采用抢占式调度模型:优先级高的线程更可能获取 CPU 时间片,优先级范围为 1~10(默认 5)。
int getPriority()
:获取优先级;void setPriority(int newPriority)
:设置优先级。
示例
public class PriorityDemo {public static void main(String[] args) {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println("线程A:" + i);}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println("线程B:" + i);}});t1.setPriority(10); // 最高优先级t2.setPriority(1); // 最低优先级t1.start();t2.start();}
}
注意:优先级只是 “概率”,并非绝对 —— 优先级高的线程不一定每次都先执行,仍受 CPU 调度随机性影响。
3.4 守护线程(Daemon Thread)
守护线程是 “后台线程”,依赖于非守护线程(如主线程)存在:当所有非守护线程结束时,守护线程会自动终止(如 JVM 的垃圾回收线程就是守护线程)。
void setDaemon(boolean on)
:将线程标记为守护线程(需在start()
前调用)。
示例
public class DaemonDemo {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("守护线程运行中...");}});daemonThread.setDaemon(true); // 标记为守护线程daemonThread.start();// 主线程执行1秒后结束try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程结束,守护线程将终止");}
}
四、线程同步:解决数据安全问题
多线程共享资源时,若多个线程同时操作共享数据,可能导致数据不一致(如卖票案例中出现重复票、负数票)。线程同步的核心是 “让多个线程有序访问共享资源”。
4.1 数据安全问题的条件
必须同时满足以下 3 个条件才会出现安全问题:
- 多线程环境;
- 存在共享数据(如卖票案例中的剩余票数);
- 多条语句操作共享数据(如 “判断票数> 0”→“卖票”→“票数 - 1”)。
4.2 同步代码块
通过synchronized
关键字将操作共享数据的代码块 “上锁”,任意时刻只有一个线程能执行该代码块。
语法
synchronized(锁对象) {// 多条操作共享数据的代码
}
- 锁对象可以是任意对象,但多个线程必须使用同一个锁对象。
卖票案例优化(同步代码块)
// 卖票线程类
public class SellTicket implements Runnable {private int tickets = 100; // 共享票数private final Object lock = new Object(); // 锁对象@Overridepublic void run() {while (true) {synchronized (lock) { // 上锁if (tickets <= 0) break;try {Thread.sleep(100); // 模拟卖票延迟} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖出第" + tickets + "张票");tickets--;} // 解锁}}
}// 测试类
public class SellTicketDemo {public static void main(String[] args) {SellTicket seller = new SellTicket();new Thread(seller, "窗口1").start();new Thread(seller, "窗口2").start();new Thread(seller, "窗口3").start();}
}
4.3 同步方法
将synchronized
关键字加到方法上,此时锁对象为:
- 非静态同步方法:锁对象是
this
; - 静态同步方法:锁对象是
类名.class
(类的字节码对象)。
语法
// 非静态同步方法
public synchronized void sell() {// 操作共享数据的代码
}// 静态同步方法
public static synchronized void sell() {// 操作共享数据的代码
}
4.4 Lock 锁(JDK5+)
synchronized
是隐式锁(自动上锁 / 解锁),而Lock
是显式锁,需手动调用lock()
上锁、unlock()
解锁,灵活性更高。常用实现类为ReentrantLock
(可重入锁)。
核心方法
void lock()
:获取锁;void unlock()
:释放锁(建议在finally
中调用,确保锁一定会释放)。
卖票案例优化(Lock 锁)
import java.util.concurrent.locks.ReentrantLock;public class SellTicketWithLock implements Runnable {private int tickets = 100;private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock(); // 上锁try {if (tickets <= 0) break;Thread.sleep(100);System.out.println(Thread.currentThread().getName() + "卖出第" + tickets + "张票");tickets--;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); // 解锁(finally确保释放)}}}
}
4.5 死锁(Deadlock)
死锁是多线程同步中的常见问题:当两个或多个线程互相持有对方需要的锁,且都不释放自己的锁时,线程会永远阻塞。
死锁示例
public class DeadlockDemo {public static void main(String[] args) {Object lockA = new Object();Object lockB = new Object();// 线程1:持有lockA,等待lockBnew Thread(() -> {synchronized (lockA) {System.out.println("线程1持有lockA,等待lockB");synchronized (lockB) {System.out.println("线程1获取lockB,执行完成");}}}).start();// 线程2:持有lockB,等待lockAnew Thread(() -> {synchronized (lockB) {System.out.println("线程2持有lockB,等待lockA");synchronized (lockA) {System.out.println("线程2获取lockA,执行完成");}}}).start();}
}
避免死锁的原则
- 减少同步嵌套;
- 按固定顺序获取锁(如线程都先获取 lockA,再获取 lockB);
- 限时释放锁(如使用
tryLock(timeout)
)。
五、经典案例:生产者消费者模型
生产者消费者模型是多线程协作的典型场景:生产者线程生产数据,消费者线程消费数据,通过共享缓冲区(如队列)解耦,实现 “生产 - 消费” 的有序协作。
5.1 基础模型:基于 wait ()/notify ()
利用 Object 类的等待 / 唤醒方法实现协作(需配合synchronized
使用):
void wait()
:让当前线程释放锁并进入等待状态;void notifyAll()
:唤醒所有等待该锁的线程。
案例实现(汉堡包生产与消费)
- 共享缓冲区(Desk 类):封装包子数量、锁对象、生产 / 消费标记;
- 生产者(Cooker 类):生产汉堡包,唤醒消费者;
- 消费者(Foodie 类):消费汉堡包,唤醒生产者。
// 共享缓冲区
public class Desk {private boolean hasBurger = false; // 是否有汉堡包private int total = 10; // 总数量private final Object lock = new Object(); // 锁对象// getter/setterpublic boolean isHasBurger() { return hasBurger; }public void setHasBurger(boolean hasBurger) { this.hasBurger = hasBurger; }public int getTotal() {