君子生非异也,善假于物也 —— [荀子]·[劝学]
一、导言
条件队列灵活,但用错也十分容易。一般来说能用
BlockingQueue
、Latch
、Semaphore
、Future
等高级工具实现的就不要直接使用条件队列。 ——<<java并发编程实战>>
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()
以及条件谓词判断时都得持有锁: 这三个条件队列依附对象的锁。