定义
英文为Guarded Suspension,用在一个线程等待另一个线程的执行结果。
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断的从一个线程传递到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK中的join,Futrued的实现采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步类
实现
模拟t1线程要吃苹果,没有苹果只能等待,等有了苹果之后线程t1就会吃掉苹果。t2线程模拟产生苹果。
1. 非超时版
wait不设置超时时间
@Slf4j
public class TestGuardedSuspension {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
try {
guardedObject.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(new Object());
},"t2").start();
}
}
@Slf4j
class GuardedObject{
// 苹果初始值为null
private Object apple;
// 锁用于保证吃与生产的同步
private final static Object lock = new Object();
//吃苹果,apple == null 时就使用wait方法等待。 apple != null时就吃掉苹果
public Object get() throws InterruptedException {
synchronized (lock){
while (apple == null){
log.debug("当前还没有苹果");
lock.wait();
}
}
log.debug("吃掉apple成功");
return apple;
}
// 产生苹果
public void complete(Object apple){
synchronized (lock){
log.debug("投喂苹果");
this.apple = apple;
lock.notifyAll();
}
}
}
预期结果为t1线程开始等待t2生产苹果,t2线程2s后产生苹果,t1消费苹果。
实际执行结果如下图,可知符合预期。
2.超时版
wait设置超时时间,假如2s还没有苹果t1线程就退出
@Slf4j
class GuardedObjectTimeOut{
private Object apple;
private final static Object lock = new Object();
public Object get(long millis) throws InterruptedException {
synchronized (lock){
// 开始时间
long begin = System.currentTimeMillis();
// 已经经历了的时间, 初始为0 , 该变量用于防止假唤醒 重复等待 millis 时间
long timePass = 0;
while (apple == null){
long waitTime = millis - timePass;
if (waitTime <= 0){
log.debug("超时了,不等了,不吃苹果了");
break;
}
log.debug("当前还没有苹果");
lock.wait(waitTime);
timePass = System.currentTimeMillis() - begin;
}
}
if (apple != null){
log.debug("吃掉apple成功");
}
return apple;
}
public void complete(Object apple){
synchronized (lock){
log.debug("投喂苹果");
this.apple = apple;
lock.notifyAll();
}
}
}
- 测试没有超时代码
t2线程等待1s
public static void main(String[] args) {
GuardedObjectTimeOut guardedObject = new GuardedObjectTimeOut();
new Thread(()->{
try {
guardedObject.get(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(new Object());
},"t2").start();
}
测试结果:
2. 测试超时代码
t2线程等待3s
public static void main(String[] args) {
GuardedObjectTimeOut guardedObject = new GuardedObjectTimeOut();
new Thread(()->{
try {
guardedObject.get(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
// 超时
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(new Object());
},"t2").start();
}
测试结果:
3. 测试假唤醒
测试假唤醒,设置t2线程1s后不投放苹果
public static void main(String[] args) {
GuardedObjectTimeOut guardedObject = new GuardedObjectTimeOut();
new Thread(()->{
try {
guardedObject.get(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
// 等待1s
Thread.sleep(1000);
// 设置投喂的苹果为null,即为不投放苹果
guardedObject.complete(null);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
测试结果:
总结
- 该模式适合于有一个结果需要从一个线程传递到另一个线程的情况下使用
- 超时版应该注意等待时间需要注意假唤醒的情况,所以应该设置一个经历时间waitpass和一个开始等待的时间begin,然后在wait后面动态的用当前时间减去begin赋值给waitpass历经时间。不这样会出现实际等待时间超过自己设置的等待时间