小故事 - 为什么要wait
- 由于条件不满足,小南不能继续进行计算
- 但小南如果一直占用着锁,其他人就得一直阻塞,效率太低
- 于是老王单开了一键休息室(调用wait方法),让小南到休息室(WaitSet)等着去了,但这是锁释放开,其他人可以由老王随机安排进屋
- 知道小M将烟送来,大叫一声【你的烟到了】(调用notify方法)
- 小南于是可以离开休息室,重新进入竞争锁的队列
原理
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,单唤醒后并不意味着立即获得锁,仍需要进入EntryList重新竞争
join原理
调用者轮询检查线程alive状态
t1.join()
等价于下面的代码
synchronized(t1) {
// 调用者线程进入t1的WaitSet等待,直到t1运行结束
while(t1.isAlive()) {
t1.wait(0);
}
}
API介绍
obj.wait()
让进入object监视器的线程到WaitSet等待obj.notify()
在object上正在WaitSet等待的线程中挑一个唤醒obj.notifyAll()
让object上正在WaitSet等待的线程全部唤醒
他们都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁,才能调用这几个方法
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码...");
}
}, "t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码...");
}
}, "t2").start();
TimeUnit.SECONDS.sleep(2);
log.debug("唤醒obj上的其他线程");
// obj.notify();
synchronized (obj) {
obj.notify();
// obj.notifyAll();
}
}
如果没有获得对象锁,则会报错
2022/03/06-03:56:00.386 [t1] c.Test1 - 执行...
2022/03/06-03:56:00.388 [t2] c.Test1 - 执行...
2022/03/06-03:56:02.386 [main] c.Test1 - 唤醒obj上的其他线程
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at waittest.Test1.main(Test1.java:43)
notify的一种结果
2022/03/06-03:57:15.787 [t1] c.Test1 - 执行...
2022/03/06-03:57:15.789 [t2] c.Test1 - 执行...
2022/03/06-03:57:17.789 [main] c.Test1 - 唤醒obj上的其他线程
2022/03/06-03:57:17.790 [t1] c.Test1 - 其他代码...
notifyAll的结果
2022/03/06-03:57:45.530 [t1] c.Test1 - 执行...
2022/03/06-03:57:45.532 [t2] c.Test1 - 执行...
2022/03/06-03:57:47.544 [main] c.Test1 - 唤醒obj上的其他线程
2022/03/06-03:57:47.544 [t2] c.Test1 - 其他代码...
2022/03/06-03:57:47.544 [t1] c.Test1 - 其他代码...
wait()
方法会释放对象的锁,进入WaitSet等待区,从而让其他线程有机会获得对象的锁。无限制等待,直到notify为止
wait(long n)
有时限的等待,到n毫秒后结束等待,或是被notify
wait notify的正确姿势
开始之前先看看
sleep(long n)
和wait(long n)
区别
1)sleep是Thread方法,而wait是Object方法
2)sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
3)sleep在睡眠时,不会释放对象锁的,但wait在等待的时候会释放对象锁
4)他们的状态都是TIMED_WAITING
step 1
思考下面的解决方案好不好,为什么?
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有没有烟?{}", hasCigarette);
if (!hasCigarette) {
log.debug("没有烟,歇会儿");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有没有烟?{}", hasCigarette);
if (hasCigarette) {
log.debug("可以干活了");
}
}
}, "t1").start();
for (int i = 0; i < 5; i++)
new Thread(() -> {
synchronized (room) {
log.debug("可以干活哦...");
}
}, "其他人").start();
Thread.sleep(1000);
new Thread(() -> {
hasCigarette = true;
log.debug("烟来喽");
}, "送烟的").start();
}
输出
2022/03/06-13:27:54.211 [t1] c.Test2 - 有没有烟?false
2022/03/06-13:27:54.213 [t1] c.Test2 - 没有烟,歇会儿
2022/03/06-13:27:55.213 [送烟的] c.Test2 - 烟来喽
2022/03/06-13:27:56.217 [t1] c.Test2 - 有没有烟?true
2022/03/06-13:27:56.217 [t1] c.Test2 - 可以干活了
2022/03/06-13:27:56.217 [其他人] c.Test2 - 可以干活哦...
2022/03/06-13:27:56.217 [其他人] c.Test2 - 可以干活哦...
2022/03/06-13:27:56.217 [其他人] c.Test2 - 可以干活哦...
2022/03/06-13:27:56.217 [其他人] c.Test2 - 可以干活哦...
2022/03/06-13:27:56.217 [其他人] c.Test2 - 可以干活哦...
- 其他干活的线程,要一直阻塞,效率太低
- t1线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了synchronized(room)后,就好比t1在里面反锁了门睡觉,烟根本没法送进门,main没加synchronized就好像main线程是翻窗户进来的
- 解决办法,使用wait-notify机制
step 2
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有没有烟?{}", hasCigarette);
if (!hasCigarette) {
log.debug("没有烟,歇会儿");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有没有烟?{}", hasCigarette);
if (hasCigarette) {
log.debug("可以干活了");
}
}
}, "t1").start();
for (int i = 0; i < 5; i++)
new Thread(() -> {
synchronized (room) {
log.debug("可以干活哦...");
}
}, "其他人").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟来喽");
room.notify();
}
}, "送烟的").start();
}
输出
2022/03/06-13:32:21.203 [t1] c.Test3 - 有没有烟?false
2022/03/06-13:32:21.203 [t1] c.Test3 - 没有烟,歇会儿
2022/03/06-13:32:21.203 [其他人] c.Test3 - 可以干活哦...
2022/03/06-13:32:21.203 [其他人] c.Test3 - 可以干活哦...
2022/03/06-13:32:21.203 [其他人] c.Test3 - 可以干活哦...
2022/03/06-13:32:21.203 [其他人] c.Test3 - 可以干活哦...
2022/03/06-13:32:21.203 [其他人] c.Test3 - 可以干活哦...
2022/03/06-13:32:22.202 [送烟的] c.Test3 - 烟来喽
2022/03/06-13:32:22.202 [t1] c.Test3 - 有没有烟?true
2022/03/06-13:32:22.202 [t1] c.Test3 - 可以干活了
- 解决了其他线程干活的线程阻塞问题
- 但如果有其他线程也在等待条件呢?
step 3
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有没有烟?{}", hasCigarette);
if (!hasCigarette) {
log.debug("没有烟,歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有没有烟?{}", hasCigarette);
if (hasCigarette) {
log.debug("可以干活了");
} else {
log.debug("干活失败");
}
}
}, "t1").start();
new Thread(() -> {
synchronized (room) {
log.debug("外卖来了吗?{}", hasCigarette);
if (!hasTakeout) {
log.debug("没外卖,歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖来了吗?{}", hasCigarette);
if (hasTakeout) {
log.debug("可以干活了");
} else {
log.debug("干活失败");
}
}
}, "t2").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖来喽");
room.notify();
}
}, "送外卖的").start();
}
输出
2022/03/06-13:37:13.816 [t1] c.Test4 - 有没有烟?false
2022/03/06-13:37:13.816 [t1] c.Test4 - 没有烟,歇会儿
2022/03/06-13:37:13.816 [t2] c.Test4 - 外卖来了吗?false
2022/03/06-13:37:13.816 [t2] c.Test4 - 没外卖,歇会儿
2022/03/06-13:37:14.823 [送外卖的] c.Test4 - 外卖来喽
2022/03/06-13:37:14.823 [t1] c.Test4 - 有没有烟?false
2022/03/06-13:37:14.823 [t1] c.Test4 - 干活失败
- notify只能随机唤醒一个WaitSet中的线程,这时如果有其他线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
- 解决方法,改为notifyAll
step 4
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖来喽");
room.notifyAll();
}
}, "送外卖的").start();
输出
2022/03/06-13:39:27.661 [t1] c.Test5 - 有没有烟?false
2022/03/06-13:39:27.661 [t1] c.Test5 - 没有烟,歇会儿
2022/03/06-13:39:27.661 [t2] c.Test5 - 外卖来了吗?false
2022/03/06-13:39:27.661 [t2] c.Test5 - 没外卖,歇会儿
2022/03/06-13:39:28.675 [送外卖的] c.Test5 - 外卖来喽
2022/03/06-13:39:28.675 [t2] c.Test5 - 外卖来了吗?false
2022/03/06-13:39:28.675 [t2] c.Test5 - 可以干活了
2022/03/06-13:39:28.675 [t1] c.Test5 - 有没有烟?false
2022/03/06-13:39:28.675 [t1] c.Test5 - 干活失败
- 用notifyAll仅解决某个线程的唤醒问题,但使用if + wait 判断仅有一次机会,一单条件不成立,就没看有重新判断的机会了
- 解决办法,用while + wait,当条件不成立,再次wait
step 5
将 if 改为 while
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有没有烟?{}", hasCigarette);
while (!hasCigarette) {
log.debug("没有烟,歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以干活了");
}
}, "t1").start();
new Thread(() -> {
synchronized (room) {
log.debug("外卖来了吗?{}", hasCigarette);
while (!hasTakeout) {
log.debug("没外卖,歇会儿");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以干活了");
}
}, "t2").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖来喽");
room.notifyAll();
}
}, "送外卖的").start();
}
输出
2022/03/06-13:43:45.737 [t1] c.Test6 - 有没有烟?false
2022/03/06-13:43:45.737 [t1] c.Test6 - 没有烟,歇会儿
2022/03/06-13:43:45.743 [t2] c.Test6 - 外卖来了吗?false
2022/03/06-13:43:45.743 [t2] c.Test6 - 没外卖,歇会儿
2022/03/06-13:43:46.751 [送外卖的] c.Test6 - 外卖来喽
2022/03/06-13:43:46.751 [t2] c.Test6 - 可以干活了
2022/03/06-13:43:46.751 [t1] c.Test6 - 没有烟,歇会儿
同步模式之保护性暂停
1、定义
即Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
- 有一个结果需要从一个线程传递给另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(生产者/消费者)
- JDK中,join的实现、Future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
2、实现
@Slf4j(topic = "c.GuardedSuspension")
public class GuardedSuspension {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object response = new Object();
log.debug("new response : {}", response);
guardedObject.complete(response);
}).start();
log.debug("wait response");
Object response = guardedObject.get();
log.debug("get response : {}", response);
}
}
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get(){
synchronized (lock) {
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}
输出
2022/03/06-14:00:11.709 [main] c.TPTInturrept - wait response
2022/03/06-14:00:13.724 [Thread-0] c.TPTInturrept - new response : java.lang.Object@77f37d84
2022/03/06-14:00:13.724 [main] c.TPTInturrept - get response : java.lang.Object@77f37d84
3、带超时版的GuardedObject
@Slf4j(topic = "c.GuardedObjectV2")
class GuardedObjectV2 {
private Object response;
private final Object lock = new Object();
public Object get(long millis) {
synchronized (lock) {
// 开始时间
long begin = System.currentTimeMillis();
// 已经过时间
long timePassed = 0;
while (response == null) {
// 还需要等待时间
long waitTime = millis - timePassed;
if (waitTime <= 0) {
log.debug("超时了...");
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
timePassed = System.currentTimeMillis() - begin;
log.debug("timePassed:{}, object is null? {} " , timePassed, response == null);
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
this.response = response;
log.debug("notify...");
lock.notifyAll();
}
}
}
测试,没有超时
public static void main(String[] args) {
GuardedObjectV2 guardedObjectV2 = new GuardedObjectV2();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObjectV2.complete(null);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObjectV2.complete(new Object());
}).start();
log.debug("wait response");
Object response = guardedObjectV2.get(2500);
log.debug("get response : {}", response);
}
输出
2022/03/06-14:32:05.620 [main] c.GuardedSuspension - wait response
2022/03/06-14:32:06.630 [Thread-0] c.GuardedObjectV2 - notify...
2022/03/06-14:32:06.630 [main] c.GuardedObjectV2 - timePassed:1010, object is null? true
2022/03/06-14:32:07.634 [Thread-0] c.GuardedObjectV2 - notify...
2022/03/06-14:32:07.634 [main] c.GuardedObjectV2 - timePassed:2014, object is null? false
2022/03/06-14:32:07.634 [main] c.GuardedSuspension - get response : java.lang.Object@3f0ee7cb
测试,超时
Object response = guardedObjectV2.get(1500);
输出
2022/03/06-14:34:23.324 [main] c.GuardedSuspension - wait response
2022/03/06-14:34:24.330 [Thread-0] c.GuardedObjectV2 - notify...
2022/03/06-14:34:24.330 [main] c.GuardedObjectV2 - timePassed:995, object is null? true
2022/03/06-14:34:24.839 [main] c.GuardedObjectV2 - timePassed:1504, object is null? true
2022/03/06-14:34:24.839 [main] c.GuardedObjectV2 - 超时了...
2022/03/06-14:34:24.839 [main] c.GuardedSuspension - get response : null
2022/03/06-14:34:25.333 [Thread-0] c.GuardedObjectV2 - notify...
4、多任务版GuardedObject
途中Futures就好比居民楼一层的信箱(每个信箱有房间编号),左侧的t0,t2,t4就好比等待邮件的居民,右侧的t1,t3,t4就好比邮递员
如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果消费者】,还能同时支持多个任务管理
新增id来表示GuardedObject
@Slf4j(topic = "c.GuardedObjectV3")
class GuardedObjectV3 {
private int id;
public GuardedObjectV3(int id) {
this.id = id;
}
public int getId() {
return id;
}
private Object response;
public Object get(long millis) {
synchronized (this) {
// 开始时间
long begin = System.currentTimeMillis();
// 已经过时间
long timePassed = 0;
while (response == null) {
// 还需要等待时间
long waitTime = millis - timePassed;
if (waitTime <= 0) {
log.debug("超时了...");
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
timePassed = System.currentTimeMillis() - begin;
log.debug("timePassed:{}, object is null? {} ", timePassed, response == null);
}
return response;
}
}
public void complete(Object response) {
synchronized (this) {
this.response = response;
log.debug("notify...");
this.notifyAll();
}
}
}
中间解耦类
class Mailboxs {
private static final Map<Integer, GuardedObjectV3> boxes = new Hashtable<>();
private static int id = 0;
private static synchronized int generateId() {
return ++id;
}
public static GuardedObjectV3 getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObjectV3 createGuardedObject() {
GuardedObjectV3 guardedObjectV3 = new GuardedObjectV3(generateId());
boxes.put(guardedObjectV3.getId(), guardedObjectV3);
return guardedObjectV3;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
业务相关类及接口
@Slf4j(topic = "c.PeopleAction")
class PeopleAction implements Runnable {
@Override
public void run() {
// 收信
GuardedObjectV3 guardedObjectV3 = Mailboxs.createGuardedObject();
log.debug("开售收信,id:{}", guardedObjectV3.getId());
Object mail = guardedObjectV3.get(5000);
log.debug("收到信了,id:{},内容:{}", guardedObjectV3.getId(), mail);
}
}
@Slf4j(topic = "c.Postman")
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
// 收信
GuardedObjectV3 guardedObjectV3 = Mailboxs.getGuardedObject(id);
log.debug("开售送信,id:{},内容:{}", guardedObjectV3.getId(), mail);
guardedObjectV3.complete(mail);
}
}
测试
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(new PeopleAction()).start();
}
Thread.sleep(1000);
for (Integer id : Mailboxs.getIds()) {
new Postman(id, "内容" + id).start();
}
}
输出
2022/03/06-15:02:11.244 [Thread-0] c.PeopleAction - 开售收信,id:1
2022/03/06-15:02:11.244 [Thread-1] c.PeopleAction - 开售收信,id:2
2022/03/06-15:02:11.244 [Thread-2] c.PeopleAction - 开售收信,id:3
2022/03/06-15:02:12.241 [Thread-4] c.Postman - 开售送信,id:2,内容:内容2
2022/03/06-15:02:12.241 [Thread-3] c.Postman - 开售送信,id:3,内容:内容3
2022/03/06-15:02:12.241 [Thread-4] c.GuardedObjectV3 - notify...
2022/03/06-15:02:12.241 [Thread-3] c.GuardedObjectV3 - notify...
2022/03/06-15:02:12.241 [Thread-5] c.Postman - 开售送信,id:1,内容:内容1
2022/03/06-15:02:12.241 [Thread-5] c.GuardedObjectV3 - notify...
2022/03/06-15:02:12.241 [Thread-2] c.GuardedObjectV3 - timePassed:997, object is null? false
2022/03/06-15:02:12.241 [Thread-1] c.GuardedObjectV3 - timePassed:997, object is null? false
2022/03/06-15:02:12.241 [Thread-0] c.GuardedObjectV3 - timePassed:997, object is null? false
2022/03/06-15:02:12.241 [Thread-1] c.PeopleAction - 收到信了,id:2,内容:内容2
2022/03/06-15:02:12.241 [Thread-0] c.PeopleAction - 收到信了,id:1,内容:内容1
2022/03/06-15:02:12.241 [Thread-2] c.PeopleAction - 收到信了,id:3,内容:内容3
异步模式之生产者/消费者
1、定义
要点
- 与前面的保护性暂停中的GuardedObject不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据如何处理,而消费者专心处理数据结果
- 消息队列是有容量限制的,满时不再加入数据,空时不再消耗数据
- JDK中各种阻塞队列,采用的就是这种模式
2、实现
class Message {
private int id;
private Object Object;
public Message(int id, java.lang.Object object) {
this.id = id;
Object = object;
}
public int getId() {
return id;
}
public java.lang.Object getObject() {
return Object;
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
private int capacity;
private final Deque<Message> queue;
public MessageQueue(int capacity) {
this.capacity = capacity;
queue = new LinkedList<>();
}
public void put(Message message) {
synchronized (queue) {
while (queue.size() == capacity) {
log.debug("队列已满,wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("生产消息,id:{},内容:{}", message.getId(), message.getObject());
queue.addLast(message);
queue.notifyAll();
}
}
public Message take() {
synchronized (queue) {
while (queue.isEmpty()) {
log.debug("没有可用的消息,wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();
log.debug("消费消息,id:{},内容:{}", message.getId(), message.getObject());
queue.notifyAll();
return message;
}
}
}
测试
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for (int i = 0; i < 4; i++) {
int finalI = i;
new Thread(() -> {
messageQueue.put(new Message(finalI, "内容" + finalI));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
Message message = messageQueue.take();
}
}, "消费者").start();
}
输出
2022/03/06-15:56:31.921 [消费者] c.MessageQueue - 没有可用的消息,wait
2022/03/06-15:56:31.921 [生产者3] c.MessageQueue - 生产消息,id:3,内容:内容3
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 消费消息,id:3,内容:内容3
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 没有可用的消息,wait
2022/03/06-15:56:31.930 [生产者1] c.MessageQueue - 生产消息,id:1,内容:内容1
2022/03/06-15:56:31.930 [生产者0] c.MessageQueue - 生产消息,id:0,内容:内容0
2022/03/06-15:56:31.930 [生产者2] c.MessageQueue - 队列已满,wait
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 消费消息,id:1,内容:内容1
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 消费消息,id:0,内容:内容0
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 没有可用的消息,wait
2022/03/06-15:56:31.930 [生产者2] c.MessageQueue - 生产消息,id:2,内容:内容2
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 消费消息,id:2,内容:内容2
2022/03/06-15:56:31.930 [消费者] c.MessageQueue - 没有可用的消息,wait