《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 进程与线程的关系
进程和线程是操作系统中两个核心概念,二者的关系可概括为 “进程包含线程,线程是进程的执行单元”:
- 单线程程序:一个进程只有一条执行路径(如早期的 Java 程序,main 方法就是单线程)。
- 多线程程序:一个进程有多条执行路径(如浏览器同时渲染页面、下载文件)。
二、Java 多线程的三种实现方式
Java 提供了三种主流的多线程实现方式,各有优缺点,适用于不同场景。
2.1 方式一:继承 Thread 类
Thread 类是 Java 中线程的基础类,通过继承它并重写run()
方法即可实现多线程。
实现步骤
- 定义类继承
Thread
; - 重写
run()
方法(封装线程执行的逻辑); - 创建线程对象,调用
start()
方法启动线程(注意:不能直接调用run()
,否则会作为普通方法执行)。
代码演示
// 自定义线程类public class MyThread extends Thread { @Override public void run() { // 线程执行逻辑:打印0-99 for (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 { @Override public 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
:泛型接口,call()
方法返回泛型类型 V,可抛出异常;FutureTask
:实现Future
接口,用于接收call()
的返回值,同时可作为Thread
的构造参数。
实现步骤
- 定义类实现
Callable
接口,重写call()
方法; - 创建
Callable
实现类对象; - 创建
FutureTask
对象,传入Callable
对象; - 创建
Thread
对象,传入FutureTask
,调用start()
; - 调用
FutureTask
的get()
方法获取线程执行结果(该方法会阻塞,直到线程执行完成)。
代码演示
// 实现Callable接口,指定返回值类型为Stringpublic class MyCallable implements Callable { @Override public 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 futureTask = new FutureTask(callable); Thread t = new Thread(futureTask); t.start(); // 获取返回结果(会阻塞直到线程结束) String result = futureTask.get(); System.out.println(\"线程返回结果:\" + result); }}
2.4 三种方式对比
Thread.currentThread()
获取线程对象三、线程常用操作
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 { 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(); // 锁对象 @Override public 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(); @Override public 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,等待lockB new Thread(() -> { synchronized (lockA) { System.out.println(\"线程1持有lockA,等待lockB\"); synchronized (lockB) { System.out.println(\"线程1获取lockB,执行完成\"); } } }).start(); // 线程2:持有lockB,等待lockA new 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/setter public boolean isHasBurger() { return hasBurger; } public void setHasBurger(boolean hasBurger) { this.hasBurger = hasBurger; } public int getTotal() {