什么是Guarded Suspension Pattern?
设想一个场景,你在家里换衣服,突然门铃响了,总不能换到一半出去开门吧,于是说:“请稍等”。换完衣服再把房门打开。
当现在不适合马上执行某一个操作的时候,就需要这个想要执行操作的线程进行等待。
这里我们用代码来说明一切:
我们模拟4个人存钱取钱的场景,银行初始金额为0。
package guardedSuspension;
public class Bank {
private int money = 0;
public Bank(){
}
public synchronized void withDraw(int money){
while(this.money-money<0){
try {
wait();//只要钱不够 就不能进入临界区
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始取钱
this.money-=money;
System.out.println(Thread.currentThread().getName()+"取钱"+money+",还剩"+this.money);
notifyAll();//唤醒所有线程
}
public synchronized void save(int money){
this.money+=money;//存钱
System.out.println(Thread.currentThread().getName()+"存了钱"+money+",现在银行还剩"+this.money+"元");
}
}
然后是实现了Runnable的target:
package guardedSuspension;
import java.util.Random;
public class PersonRunnable implements Runnable {
private Bank bank;
PersonRunnable(Bank bank){
this.bank = bank;
}
@Override
public void run() {
Random random = new Random();
int money = random.nextInt(10);//0-9随机数
bank.save(money);//存钱
int money2 = random.nextInt(10);//0-9随机数
bank.withDraw(money2);//取钱
}
}
最后写一个测试类:
package guardedSuspension;
public class Test {
public static void main(String[] args) {
Bank bank = new Bank();
PersonRunnable t1 = new PersonRunnable(bank);
new Thread(t1,"Diana").start();
new Thread(t1,"Tommy").start();
new Thread(t1,"Ginna").start();
new Thread(t1,"Jerry").start();
}
}
下面是一种运行结果:
现在我们开始来看代码,对于withDraw方法,存在着一个“警戒条件”,也就是只有取钱完money仍然大于0才能执行。
当线程抵达代码段while(this.money-money>0)时,会分成满足警戒条件和不满足警戒条件两种情况,警戒条件(this.money-money>=0)成立时,代码不会进入while块,而是直接进入下一个语句,直接取钱,而不会wait,警戒条件不成立,才会执行wait。
执行wait后线程等着被notify/notifyAll,然而更根本的时,线程在等待警戒条件的成立,等到收到notify/notifyAll时再判断警戒条件成立不成立,如果成立就取钱,否则继续进入wait set。这就是为什么有时候(比如上方的运行结果)程序迟迟不会结束的原因,因为所有人都存钱完了,然后还是不够取,所以那个没有足够钱取的线程就一直呆在wait set了。
整理一下,关于警戒条件的架构使用方法是:
while("警戒条件"的逻辑否定){
使用wait等待
}
进行"目的操作"
该模式的时序图如下:
如何理解Guarded Suspension Pattern?
1、有条件的synchronized,在Single Threaded Execution的基础上,只要有一个线程进入临界区间,其他线程就不能进入,而是等待。因此可以称为有条件的synchronized。
2、多线程版的if。多个线程不符合警戒条件就不执行,否则就执行。
忘记更改状态和生命性
wait中的线程会在被notify/notifyAll的时候重新测试警戒条件,如果始终达到不了警戒条件则依然会进入wait set。
如果忘了修改GuardedObject参与者(比如上面的money)的状态,会导致警戒条件永远无法成立,使得线程无法继续前进,使得程序失去生命性。
各种实现方法
1、guarded wait,就像本例的实现,使用wait等待,直到被notify/notifyAll后再次测试警戒条件。使用wait等待的时间,其实就是停止在等待区里停止执行,所以不会浪费Java执行环境的处理时间:
//等待端的范例
while(!ready){
wait();
}
//唤醒端的范例
ready = true;
notifyAll();
2、busy wait(忙等待),线程不使用wait方法等待,而是使用yield方法(把当前运行机会交给优先级高的线程),然后不断测试条件的实现方法。因此等待中的线程还是运行这,会浪费jvm的时间。yield是Thread的类方法,这个方法不会解除锁,所以这个方法不应该写在synchronized里,否则容易死锁。yield只是放弃当前cpu的使用,让给其他线程,当前线程置为可运行状态。也就是说虽然当前线程调用了yield,还是可能接下来是自己运行,正确示例:
//等待端的范例
while(!ready){
Thread.yield();
}
//唤醒端的范例
ready = true;
//没有notify/notifyAll
3、spin lock(自旋锁)。旋转而锁定的意思,表现出条件成立前while循环不断旋转的样子。spin lock有时候意思和guarded wait相同,有时候和guarded wait相同。有时候一开始是busy wait,之后换成guarded wait的方式。另外,有些以硬件实现的同步机制,也称为spin lock。
4、polling(进行调查)。反复检查某个事件是否发生,当发生时,就进行对应处理的方式。