> 技术文档 > 《Java 多线程全面解析:从基础到生产者消费者模型》

《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()方法即可实现多线程。

实现步骤
  1. 定义类继承Thread
  2. 重写run()方法(封装线程执行的逻辑);
  3. 创建线程对象,调用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 接口的方式。

实现步骤
  1. 定义类实现Runnable接口;
  2. 重写run()方法;
  3. 创建Runnable实现类对象,作为参数传入Thread构造器;
  4. 调用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的构造参数。
实现步骤
  1. 定义类实现Callable接口,重写call()方法;
  2. 创建Callable实现类对象;
  3. 创建FutureTask对象,传入Callable对象;
  4. 创建Thread对象,传入FutureTask,调用start()
  5. 调用FutureTaskget()方法获取线程执行结果(该方法会阻塞,直到线程执行完成)。
代码演示
// 实现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 类 编程简单,可直接调用 Thread 的方法(如 getName ()) 单继承限制,扩展性差 实现 Runnable 接口 无继承限制,扩展性强,支持多个线程共享资源 需通过Thread.currentThread()获取线程对象 实现 Callable 接口 支持返回值和异常抛出,功能最完善 编程较复杂,需配合 FutureTask 使用

三、线程常用操作

3.1 设置与获取线程名称

线程名称用于标识线程,默认名称为Thread-0Thread-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 个条件才会出现安全问题:

  1. 多线程环境
  2. 存在共享数据(如卖票案例中的剩余票数);
  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(); }}
避免死锁的原则
  1. 减少同步嵌套;
  2. 按固定顺序获取锁(如线程都先获取 lockA,再获取 lockB);
  3. 限时释放锁(如使用tryLock(timeout))。

五、经典案例:生产者消费者模型

生产者消费者模型是多线程协作的典型场景:生产者线程生产数据,消费者线程消费数据,通过共享缓冲区(如队列)解耦,实现 “生产 - 消费” 的有序协作。

5.1 基础模型:基于 wait ()/notify ()

利用 Object 类的等待 / 唤醒方法实现协作(需配合synchronized使用):

  • void wait():让当前线程释放锁并进入等待状态;
  • void notifyAll():唤醒所有等待该锁的线程。
案例实现(汉堡包生产与消费)
  1. 共享缓冲区(Desk 类):封装包子数量、锁对象、生产 / 消费标记;
  2. 生产者(Cooker 类):生产汉堡包,唤醒消费者;
  3. 消费者(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() {