java并发-条件队列-wait、notify、notifyall

java并发-条件队列-wait、notify、notifyall-飞一样的编程
飞一样的编程
擅长邻域:Java,MySQL,Linux,nginx,springboot,mongodb,微信小程序,vue

2021-01-01 23:54:12   316浏览 分类: Java

一、前言
条件队列灵活,但用错也十分容易。一般来说能用BlockingQueue、Latch、Semaphore、Future等高级工具实现得就不要直接使用条件队列。 ——<>

java得内置得条件队列存在一些缺陷,每个内置锁(基于synchronize块)都只能有一个关联得条件队列,因此可能存在多个线程因不同得条件谓词不满足而在同一个条件队列上。这个特性很可能就会导致"通知丢失"(不使用notifyall下)。就算使用了notifyall也会因为锁得争抢导致一些无谓得cpu资源得浪费。

二、基本使用
ps: 本篇博文代码使用了lombok得@SneakThrow注解,作用就是避免各种在方法头上得异常声明

2.1. wait()
wait()方法会自动释放锁,进入等待队列,直到被唤醒。线程被唤醒后会重新请求锁,它和其他尝试进入synchronize块得线程没有区别。

2.1.1. 不持有当前锁调用wait会报错
@Test
@SneakyThrows
public void waitSt() {
// 必须在synchronized代码块里,否则会报错: java.lang.IllegalMonitorStateException
synchronized (this) {
wait();
System.out.println("这句话永远不可能输出");
}
}

@Test
@SneakyThrows
public void waitSt4() {
Object o = new Object();
synchronized (o) {
//加载不同逻辑块会报错
wait();
}
}
2.1.2. wait会被中断唤醒
@Test
@SneakyThrows
public void waitSt2() {
synchronized (this) {
Thread thread = Thread.currentThread();
Runnable runnable = new Runnable() {
@Override
@SneakyThrows
public void run() {
// 睡眠一秒后唤醒main线程
Thread.sleep(1_000);
thread.interrupt();
}
};
new Thread(runnable).start();
wait(); // 被中断打断等待,直接报错退出程序
System.out.println("这句话永远不可能输出");
}
}
2.1.3. wait被notify唤醒
@Test
@SneakyThrows
public void waitSt3() {
// 必须在synchronized代码块里,否则会报错: java.lang.IllegalMonitorStateException
synchronized (this) {
Thread thread = Thread.currentThread();
Runnable runnable = new Runnable() {
@Override
@SneakyThrows
public void run() {
// 睡一秒后唤醒main线程
Thread.sleep(1_000);
synchronized (ThreadST.this) /*必须被synchronized住*/ {
ThreadST.this.notify();
}
}
};
new Thread(runnable).start();

/*其实被notify唤醒后还有其他得条件判断,因为被唤醒得可能有多种,还可能在被notify和从wait唤醒之间,状态又变了*/
wait(); // 被另一个线程notify
System.out.println("这句话永远可以输出");
}
}
2.1.4 小结
wait()方法得使用会有过早唤醒、误唤醒得问题,可能被其他线程notify(),进而去争锁、抢占cpu,但很可能不是因为它预设得唤醒条件而导致得唤醒,所以wait()唤醒后需要进行条件判断,且wait()方法应该在循环中调用。

可以这样比喻: 小明使用代码事项app给自己设置了“购物”、“看电影”、“健身”等代办事项,当代办事项app响铃了,提示小明此时有代办事项,小明应该查看下是什么代办事项。
synchronized(对象){ // 获取锁失败,线程会加入到同步队列中 
while(条件不满足){
对象.wait();// 调用wait方法当前线程加入到条件队列中
}
}
2.2. notify()和notifyAll()
一句话总结区别: notifyAll()方法唤醒所有 wait 线程, notify()方法只随机唤醒一个 wait 线程。一般来说,使用notifyall()要好于使用notify。仅仅使用notify()可能会导致通知丢失得情况: 通知错了对象,而真正应该收到通知得没有接到通知(没有被唤醒)。

tips: notify后要尽快得释放锁,避免从wait()中返回得线程阻塞。

2.2.1 使用notifyall和wait实现阻塞队列
final/*禁止继承,防止子类误用导致线程不安全*/ class QueueNotifySt<T> {
private final int maxLength;
private ArrayList<T> queue;

public QueueNotifySt(int maxLength) {
this.maxLength = maxLength;
queue = new ArrayList<>(maxLength);
}

// 放
@SneakyThrows
public synchronized void put(T data) {
// 满则等待+退出锁
while (maxLength == queue.size()) {
wait();
}
queue.add(data);
notifyAll();
}

//取
@SneakyThrows
public synchronized T take() {
// 空则等待+退出锁
while (queue.size() == 0) {
wait();
}
T res = queue.remove(0);
notifyAll();
return res;
}
}
测试代码如下所示:
@Test
@SneakyThrows
public void takeTest() {
QueueNotifySt<Integer> queue = new QueueNotifySt<>(3);
new Thread(() -> queue.put(1)).start();
System.out.println("queue.take() = " + queue.take()); //输出1
queue.take();
System.out.println("不会输出");// 不会执行到这一步
}三、总结
生产环境永远在循环中调用wait()方法
永远在调用wait()前测试条件谓词,被notify()或notofyall()唤醒后也要测试条件谓词,若谓词不满足则继续wait()
调用wait()、notify()、notifyall()以及条件谓词判断时都得持有锁: 这三个条件队列依附对象得锁。
行,今天就给大家分享到这里吧,您的一份支持就是我最大的动力,最后打个小广告,我们程序员在学习和工作中或多或少会遇到一些比较棘手的问题,也就所谓的一时半会解决不了的bug,可以来杰凡IT问答平台上提问,平台上大佬很多可以快速给你一对一解决问题,有需要的朋友可以去关注下,平台网址: https://www.jf3q.com

好文章就要一起分享哦!分享海报

此处可发布评论

评论(0

暂无评论,快来写一下吧
客服QQ 1913284695