当前位置: 首页 > news >正文

《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 {@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 接口的方式。

实现步骤
  1. 定义类实现Runnable接口;
  2. 重写run()方法;
  3. 创建Runnable实现类对象,作为参数传入Thread构造器;
  4. 调用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的构造参数。
实现步骤
  1. 定义类实现Callable<V>接口,重写call()方法;
  2. 创建Callable实现类对象;
  3. 创建FutureTask对象,传入Callable对象;
  4. 创建Thread对象,传入FutureTask,调用start()
  5. 调用FutureTaskget()方法获取线程执行结果(该方法会阻塞,直到线程执行完成)。
代码演示
// 实现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-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 < 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 个条件才会出现安全问题:

  1. 多线程环境
  2. 存在共享数据(如卖票案例中的剩余票数);
  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();}
}
避免死锁的原则
  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/setterpublic boolean isHasBurger() { return hasBurger; }public void setHasBurger(boolean hasBurger) { this.hasBurger = hasBurger; }public int getTotal() {
http://www.dtcms.com/a/339207.html

相关文章:

  • 基于Paddle和YOLOv5实现 车辆检测
  • Markdown to PDF/PNG Converter
  • 浅看架构理论(二)
  • 儒释道中的 “不二” 之境:超越对立的智慧共鸣及在软件中的应用
  • Linux的基本操作
  • AC 内容审计技术
  • UE5 使用RVT制作地形材质融合
  • 【LeetCode】3655. 区间乘法查询后的异或 II (差分/商分 + 根号算法)
  • 部署Qwen-Image
  • 【AAOS】Android Automotive 16模拟器源码下载及编译
  • 【LeetCode题解】LeetCode 153. 寻找旋转排序数组中的最小值
  • HJ2 计算某字符出现次数
  • C语言关于函数传参和返回值的一些想法2(参数可修改的特殊情况)
  • 从数据孤岛到实时互联:Canal 驱动的系统间数据同步实战指南
  • 在职老D渗透日记day21:sqli-labs靶场通关(第27a关)get联合注入 过滤select和union “闭合
  • C# 13 与 .NET 9 跨平台开发实战(第一章:开发环境搭建与.NET概述)
  • Milvus 向量数据库中的索引类型
  • SQL 语句进阶实战:从基础查询到性能优化全指南
  • K8s命名空间:资源隔离与管理的核心
  • 轻量级milvus安装和应用示例
  • 一文精通 Swagger 在 .NET 中的全方位配置与应用
  • 软件测试-Selenium学习笔记
  • Dify-MCP服务创建案例
  • 循环高级综合练习①
  • 46 C++ STL模板库15-容器7-顺序容器-双端队列(deque)
  • 人工智能统一信息结构的挑战与前景
  • Vue3编程中更多常见书写错误场景
  • 使用OpenCV计算灰度图像的质心
  • 云原生堡垒机渗透测试场景
  • 所有普通I/O口都支持中断的51单片机@Ai8051U, AiCube 图形化配置