Java学习——day27(线程间通信与死锁防范)
文章目录
- 1. 线程间通信
- 1.1 基本原理
- 1.2 使用场景
- 2. 死锁与防范
- 2.1 死锁产生的原因
- 2.2 避免策略
- 3. 实践:生产者—消费者模型示例
- 3.1 完整示例代码
- 3.2 代码详解
- 4. 总结与思考
1. 线程间通信
1.1 基本原理
-
wait() 方法
当线程调用对象上的 wait() 方法时,当前线程会释放该对象的锁,并进入等待状态,直到其他线程调用同一对象上的 notify() 或 notifyAll() 方法。
注意:必须在 synchronized 块中调用 wait()。 -
notify() 方法
当线程调用 notify() 方法时,会随机唤醒等待该对象锁的一个线程,使其重新进入可运行状态。
注意:同样需要在 synchronized 块中调用。 -
notifyAll() 方法
与 notify() 类似,但会唤醒所有等待该对象锁的线程。
一般在生产—消费者模型中,唤醒所有等待的线程以便竞争资源。
1.2 使用场景
-
线程间协调 :例如在生产者—消费者模型中,生产者生产数据后通知消费者;消费者数据不足时,等待生产者通知。
-
保证顺序与互斥 :通过 wait/notify 机制实现线程按照预定顺序操作共享数据,确保数据一致性。
2. 死锁与防范
2.1 死锁产生的原因
-
死锁(Deadlock) :多个线程相互等待对方释放资源,导致所有线程都无法继续执行。
-
死锁的四个必要条件:
1.互斥条件 :资源不能共享。
2.请求与保持条件 :线程已获得资源,但仍在等待其它资源,同时保持已获得资源。
3.不剥夺条件 :资源不能被强制收回,只能在任务完成后由线程主动释放。
4.循环等待条件 :存在一组线程,形成循环等待资源的关系。
2.2 避免策略
- 破坏循环等待条件 :设定资源获取顺序,所有线程按照固定次序获取资源。
- 加锁超时 :使用可中断的锁申请,当等待时间超时后放弃当前申请,避免死锁。
- 减少锁的粒度 :尽量减少临界区代码,使用细粒度锁降低冲突几率。
- 使用死锁检测工具 :监控程序运行时的线程状态,以便及时发现和解决死锁。
3. 实践:生产者—消费者模型示例
下面的示例中,我们实现一个简单的生产者—消费者模型,使用共享缓冲区作为资源,生产者和消费者通过 wait/notify 协调工作。
代码中包含三个部分:
- Buffer 类 :共享缓冲区,内部使用队列存储数据,提供 synchronized 方法进行生产和消费操作。
- Producer 类 :生产者线程,不断向缓冲区添加数据,当缓冲区满时等待。
- Consumer 类 :消费者线程,不断从缓冲区取出数据,当缓冲区为空时等待。
3.1 完整示例代码
// ProducerConsumerDemo.java
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerDemo {
public static void main(String[] args) {
// 创建共享缓冲区,容量为 5
Buffer buffer = new Buffer(5);
// 创建生产者和消费者线程
Thread producerThread = new Thread(new Producer(buffer), "Producer");
Thread consumerThread = new Thread(new Consumer(buffer), "Consumer");
// 启动线程
producerThread.start();
consumerThread.start();
}
}
// 共享缓冲区类,使用 wait/notify 协调生产者与消费者
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
public Buffer(int capacity) {
this.capacity = capacity;
}
// 生产数据:当缓冲区满时等待,否则添加数据,并调用 notifyAll 通知消费者
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + " 等待,缓冲区已满!");
wait(); // 释放锁并等待
}
queue.add(value);
System.out.println(Thread.currentThread().getName() + " produced: " + value);
notifyAll(); // 通知等待的线程
}
// 消费数据:当缓冲区为空时等待,否则取出数据,并调用 notifyAll 通知生产者
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 等待,缓冲区为空!");
wait();
}
int value = queue.poll();
System.out.println(Thread.currentThread().getName() + " consumed: " + value);
notifyAll();
return value;
}
}
// 生产者线程类
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
int value = 0;
while (true) {
try {
buffer.produce(value++);
Thread.sleep(500); // 模拟生产过程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者线程类
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
try {
buffer.consume();
Thread.sleep(1000); // 模拟消费过程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
运行 ProducerConsumerDemo 后,控制台输出可能类似如下(注意线程调度具有随机性,具体输出顺序可能交替变化):
Producer produced: 0
Consumer consumed: 0
Producer produced: 1
Producer produced: 2
Producer produced: 3
Producer produced: 4
Producer produced: 5
Producer 等待,缓冲区已满!
Consumer consumed: 1
Producer produced: 6
Consumer consumed: 2
Consumer consumed: 3
...
说明:
-
生产者:
- 会先产生数据(例如 0、1、2…)并打印 “Producer produced: X”。
- 当缓冲区中的数据达到容量(容量设置为 5)时,生产者会打印 “Producer 等待,缓冲区已满!” 并调用 wait() 进入等待状态,直到消费者取走数据释放空间。
-
消费者:
- 当缓冲区非空时消费数据,打印 “Consumer consumed: X”,当缓冲区为空时会打印 “Consumer 等待,缓冲区为空!” 并调用 wait()。
- 由于消费者休眠时间较长(1000ms),通常会使得生产者较快地填满缓冲区,从而观察到生产者“等待”的输出。
-
协调工作:
- 每当生产者成功生产数据后调用 notifyAll(),唤醒等待中的消费者;同理,消费者消费数据后调用 notifyAll() 来唤醒等待生产者。
- 随着线程不断重复调用生产和消费操作,整个系统保持一个平衡状态,确保数据不会丢失或重复。
3.2 代码详解
-
Buffer 类:
- 使用
synchronized
修饰方法,保证同一时刻只有一个线程能够操作队列。 - 生产者调用
produce
方法时,若缓冲区已满(队列大小达到容量),调用wait()
进入等待状态,并打印相应信息;成功生产后调用notifyAll()
通知其它等待线程。 - 消费者调用
consume
方法时,若缓冲区为空,调用wait()
进入等待状态;成功消费后调用notifyAll()
进行通知。
- 使用
-
Producer 与 Consumer 类:
- 生产者线程不断生产数据,并调用缓冲区的
produce
方法。 - 消费者线程不断消费数据,并调用缓冲区的
consume
方法。 - 模拟生产与消费过程中的延时,通过
Thread.sleep()
模拟执行耗时,便于观察线程间的协调效果。
- 生产者线程不断生产数据,并调用缓冲区的
-
ProducerConsumerDemo 主类:
- 在
main
方法中,创建共享的 Buffer 实例,并启动一个生产者和一个消费者线程。运行过程中,通过等待和通知,生产者与消费者在共享缓冲区间交替执行,避免了数据丢失和竞争问题。
- 在
4. 总结与思考
-
线程间通信:
- 通过·
wait()
、notify()
和notifyAll()
实现线程间的协调,确保在临界区内的线程能够互相告知状态变化,从而达到数据共享和顺序控制。
- 通过·
-
死锁防范:
- 学习了死锁的四个必要条件,并通过设计合理的同步代码(避免不必要的嵌套锁定和严格的锁获取顺序)来防止死锁的发生。
- 编写协作代码时,应尽量缩小 synchronized 块的范围,确保及时释放锁。