1、线程间通信
线程间通信是指多个线程对共享资源的操作和协调。在生产者-消费者模型中,生产者和消费者是不同种类的线程,他们对同一个资源(如队列)进行操作。生产者负责向队列中插入数据,消费者负责从队列中取出数据。
主要挑战在于如何在资源达到上限时让生产者等待,而在资源达到下限时让消费者等待。线程间的这种相互调度,就是线程间通信。
以现实生活为例。消费者和生产者就像两个线程,原本做着各自的事情,厂家管自己生产,消费者管自己买,一般情况下彼此互不影响。900 240
但当物资到达某个临界点时,就需要根据供需关系适当作出调整。比如,当厂家做了一大堆东西,产能过剩时,应该暂停生产,扩大宣传,让消费者过来消费。
同理,当消费者发现某个热销商品售罄,应该提醒厂家尽快生产。
在上面的案例中,生产者和消费者是不同种类的线程,一个负责存入,另一个负责取出,且它们操作的是同一个资源。但最难的部分在于:资源到达上限时,生产者等待,消费者消费;资源达到下限时,生产者生产,消费者等待。
我们可以发现,原本互不打扰的两个线程之间开始了 “沟通”:
生产者:做的商品太多了,应该扩大宣传,让大家来买。
消费者:都卖完啦,应当提醒商家尽快补货。
这种线程间的相互调度,也就是线程间通信。
2、线程间通信的实现
实现线程间通信的方式有多种:
轮询:生产者和消费者线程通过循环不断检查队列的状态。这种方法简单,但会消耗大量 CPU 资源,且无法保证原子性。
等待唤醒机制(wait/notify):通过 wait 和 notify 机制,线程可以在队列为空或满时阻塞自己,当状态改变时由其他线程唤醒。synchronized 保证了线程的原子性,但 notify 可能导致线程竞争不均。
等待唤醒机制(Condition):使用ReentrantLock和Condition实现等待唤醒机制,可以更加精确地控制线程的阻塞和唤醒。通过创建不同的Condition实例,可以分别管理生产者和消费者的等待状态,避免了notify的随机唤醒问题。
2.1、轮询
设计理念:生产者和消费者线程通过循环不断检查队列的状态,队列为空时生产者才可插入数据,队列不为空时消费者才能取出数据,否则一律 sleep 等待。
代码实现:
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
/**
* 自定义阻塞队列实现:轮询版本
*
* @param <T> 队列中存储的元素类型
*/
public class WhileQueue<T> {
// 用来存储元素的容器
private final LinkedList<T> queue = new LinkedList<>();
// 队列的最大容量
private final int MAX_SIZE = 1;
/**
* 将元素添加到队列中
*
* @param resource 要插入的元素
* @throws InterruptedException 如果当前线程被中断
*/
public void put(T resource) throws InterruptedException {
// 如果队列满了,生产者线程将进入轮询等待状态
while (queue.size() >= MAX_SIZE) {
System.out.println("生产者:队列已满,无法插入...");
TimeUnit.MILLISECONDS.sleep(1000); // 线程等待1秒钟再重试
}
// 插入元素到队列的前面
System.out.println("生产者:插入" + resource + "!!!");
queue.addFirst(resource);
}
/**
* 从队列中取出元素
*
* @throws InterruptedException 如果当前线程被中断
*/
public void take() throws InterruptedException {
// 如果队列为空,消费者线程将进入轮询等待状态
while (queue.size() <= 0) {
System.out.println("消费者:队列为空,无法取出...");
TimeUnit.MILLISECONDS.sleep(1000); // 线程等待1秒钟再重试
}
// 从队列的末尾取出元素
System.out.println("消费者:取出消息!!!");
queue.removeLast();
TimeUnit.MILLISECONDS.sleep(5000); // 模拟消费操作需要时间
}
}
测试:
/**
* 测试类:创建生产者和消费者线程来测试WhileQueue的功能
*/
public class Test {
public static void main(String[] args) {
// 创建一个WhileQueue实例
WhileQueue<String> queue = new WhileQueue<>();
// 创建并启动生产者线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
queue.put("消息" + i); // 插入消息到队列
} catch (InterruptedException e) {
e.printStackTrace(); // 捕获并打印中断异常
}
}
}
}).start();
// 创建并启动消费者线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
queue.take(); // 从队列中取出消息
} catch (InterruptedException e) {
e.printStackTrace(); // 捕获并打印中断异常
}
}
}
}).start();
}
}
由于设定了队列最多只能存1个消息,所以只有当队列为空时,生产者才能插入数据。这是最简单的线程间通信:多个线程不断轮询共享资源,通过共享资源的状态判断自己下一步该做什么。
但上面的实现方式存在一些缺点:
轮询的方式太耗费 CPU 资源,如果线程过多,比如几百上千个线程同时在那轮询,会给 CPU 带来较大负担
无法保证原子性(代码里没有演示,但理论上确实如此,如果生产者的操作非原子性,消费者极可能获取到脏数据)