一、应用场景
1.多线程与锁机制是并发编程中非常重要的概念。在多线程环境下,为了保证数据的一致性和避免竞态条件,需要使用锁来对共享资源进行保护。
2.在多线程编程中,生产者-消费者模型是一个经典的场景。生产者负责生产数据,消费者负责消费数据。为了确保生产者和消费者的安全性,需要使用锁来同步它们之间的操作。
3.在Java中,可以使用synchronized关键字或者ReentrantLock类来实现锁机制。它们的区别主要在于灵活性和功能上的不同。
synchronized:synchronized是Java中内置的关键字,用于实现同步代码块或同步方法。在使用synchronized时,编译器会自动添加monitorenter和monitorexit指令,来实现对代码块或方法的加锁和解锁。synchronized比较简单易用,但功能上相对较为受限。
ReentrantLock:ReentrantLock是JDK中提供的一个显式锁,在使用时需要手动进行加锁和解锁的操作。相比synchronized,ReentrantLock更加灵活,提供了更多的功能,比如可重入性、公平性、线程中断等。但是需要注意的是,在使用ReentrantLock时需要确保在finally块中进行解锁操作,以避免出现死锁的情况。
4. JVM内存模型是指Java虚拟机在执行Java程序时管理内存的规范。Java内存模型规定了线程如何与内存交互,主要包括主内存、工作内存、内存屏障等概念。了解JVM内存模型有助于理解多线程并发编程中内存可见性、原子性、有序性等问题。
5. 在实现生产者-消费者模型时,可以使用锁机制来保证生产者和消费者之间的线程安全。在代码中要注意对共享资源的加锁和解锁操作,避免出现数据竞争和不一致的情况。在提交代码到GitHub时,可以在注释中说明所使用的锁优化策略,以便他人阅读和理解代码的逻辑。
二、生产者-消费者模型实现
完整代码示例:
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerDemo {
private static final int MAX_CAPACITY = 5; // 缓冲区最大容量
private final LinkedList<Integer> buffer = new LinkedList<>();
private final ReentrantLock lock = new ReentrantLock(); // 显式锁
private final Condition notFull = lock.newCondition(); // 缓冲区未满条件
private final Condition notEmpty = lock.newCondition(); // 缓冲区非空条件
// 生产者线程
class Producer implements Runnable {
private int itemCount = 0;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
lock.lock(); // 获取锁
try {
while (buffer.size() == MAX_CAPACITY) { // 检查缓冲区是否已满
System.out.println("缓冲区已满,生产者等待...");
notFull.await(); // 释放锁并等待
}
int item = ++itemCount;
buffer.add(item);
System.out.println("生产: " + item + " | 缓冲区大小: " + buffer.size());
notEmpty.signalAll(); // 唤醒消费者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁
}
try {
Thread.sleep(500); // 模拟生产耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// 消费者线程
class Consumer implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
lock.lock();
try {
while (buffer.isEmpty()) { // 检查缓冲区是否为空
System.out.println("缓冲区为空,消费者等待...");
notEmpty.await(); // 释放锁并等待
}
int item = buffer.removeFirst();
System.out.println("消费: " + item + " | 缓冲区大小: " + buffer.size());
notFull.signalAll(); // 唤醒生产者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
try {
Thread.sleep(1000); // 模拟消费耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ProducerConsumerDemo demo = new ProducerConsumerDemo();
// 创建多个生产者和消费者
Thread producer1 = new Thread(demo.new Producer(), "生产者1");
Thread producer2 = new Thread(demo.new Producer(), "生产者2");
Thread consumer1 = new Thread(demo.new Consumer(), "消费者1");
Thread consumer2 = new Thread(demo.new Consumer(), "消费者2");
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
Thread.sleep(10_000); // 运行10秒后终止
producer1.interrupt();
producer2.interrupt();
consumer1.interrupt();
consumer2.interrupt();
}
}
运行演示
1.初始阶段
- 生产者快速填充缓冲区至最大值(5),输出类似
生产: 1 | 缓冲区大小: 1
生产: 2 | 缓冲区大小: 2
...
生产: 5 | 缓冲区大小: 5
缓冲区已满,生产者等待...
- 消费者开始消费,缓冲区逐渐减少
2.动态平衡阶段
- 生产者和消费者交替运行。例如:
消费: 3 | 缓冲区大小: 4
生产: 6 | 缓冲区大小: 5
缓冲区已满,生产者等待...
3.终止阶段
- 主线程休眠10秒后中断所有线程,程序结束。
代码解析与设计要点
1.核心组件
- 缓冲区(buffer):
使用 LinkedList 作为共享资源,实现先进先出(FIFO)队列。容量限制通过Max_CAPACITY控制。
-
显示锁 (ReentrantLock):
替代synchronized,提供更灵活的锁控制,支持中断和超时。
-
条件变量(Condition):
*notFull:生产者等待缓冲区未满条件。
* notEmpty:消费者等待缓冲区非空条件。
替代 wait/notify,允许精确唤醒特定类型线程
2.生产者逻辑
1.获取锁:同生产者
2.检查缓冲区状态
若缓冲区已满,调用 notFull.await() 释放锁并进入等待队列
3.生产数据
等待数据到缓冲区,并打印生产信息。
4.唤醒消费者
notEmpty.signalAll() 通知消费者可以消费
5.释放锁
finally 块保证锁一定被释放,避免死锁
6.模拟耗时操作
Thread.sleep(500)降低生产者速度,平衡生产消费速率
3.消费者逻辑
1.获取锁:同生产者
2.检查缓冲区状态
- 若缓冲区为空,调用 notEmpty.await() 等待数据
3.消费数据
- 移除并处理缓冲区头部数据,打印消费信息
4.唤醒生产者
- notFull.signalAll() 通知消费者可以继续生产
5.模拟耗时操作
- Thread.sleep(1000) 模拟消费时间,通常比生产者更慢
4.主程序控制
- 多线程启动:支持多个生产者和消费者并发运行
- 优雅终止:通过 interrupt() 中断线程,结合 Thread.sleep 控制运行时间
任务验收:
1.生产者-消费者模型验收:
- 使用ReetrantLock+Condition实现
- 缓冲区大小限制(防止内存溢出)
- 包含公平锁配置
- 异常处理机制(tryLock+超时机制)
2.锁机制验收
- 功能特性对比(公平性、条件变量等)
- 性能对比说明(不同JDK版本差异)
- 使用场景建议(简单VS复杂场景)
三、synchronized vs ReentrantLock对比分析
1. 实现机制对比
| 特性 | synchronized | ReentrantLock |
|-------------------|-----------------------|-----------------------|
| 锁获取方式 | JVM隐式管理 | API显式调用 |
| 中断支持 | 不支持 | lockInterruptibly() |
| 公平锁 | 非公平 | 可配置公平策略 |
| 条件变量 | 单个监视器 | 多Condition对象 |
2. 使用场景建议
- **synchronized**:简单同步场景、自动资源管理
- **ReentrantLock**:需要细粒度控制、超时机制、公平锁等高级功能
四、关键设计优化
-
避免虚假唤醒:使用 while 而非 if 检查条件,确保线程被唤醒后再次验证状态。
-
精确唤醒:Condition 的 signalAll() 仅唤醒对应条件的线程(如消费者唤醒生产者),减少无效竞争。
-
中断处理:在 catch (InterruptedException) 中重置中断标志,确保上层逻辑能感知中断。
扩展对比
BlockingQueue 简化版:
使用 ArrayBlockingQueue 可替代手动同步逻辑(如 put() 和 take() 方法自动阻塞),但隐藏了底层锁机制。
synchronized 基础版:
代码更简洁,但无法区分唤醒条件,可能导致性能损耗。