保护性暂停(Guarded Suspension)模式是让一个线程等待另一个线程的结果。java中的join、Future、FutureTask均采用了该模式实现。
join()
join()方法使得调用该方法的线程等待另一个线程的结束,比如下面的例子:
public static void main(String[] args){
Thread t = new Thread(()->{
Thread.sleep(1000);
});
t.join();
System.out.println("1000");
}
主线程会等待线程t结束后再继续执行,所以说在程序执行1s后主程序才会打印1000。
join()的原理
// 无参数的join,调用join(0)
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 无参的join方法会调用join(0),从而进入该分支
if (millis == 0) {
// 判断线程是否还存活,存活则调用wait等待
while (isAlive()) {
wait(0);
}
} else {
// 判断线程是否还存活
while (isAlive()) {
// 计算剩余时间
long delay = millis - now;
// <=0 表示join等待已经超时,退出等待
if (delay <= 0) {
break;
}
// 使用带有时间的wait方法等待
wait(delay);
// 计算已过去的时间
now = System.currentTimeMillis() - base;
}
}
}
// native方法,它会使线程进入等待,直到通过notify唤醒或者超时
public final native void wait(long timeout) throws InterruptedException;
通过阅读源码我们发现,如果我们调用无参数的join其实是调用了join(0),所以我们直接看有参数的join方法。
该方法分为两个分支,一个是millis=0,即无限时等待。另一个是millis>0,即限时等待。
因为无限时的等待很简单,这里只分析有限时的等待。我们只需要搞清楚两个问题就能搞懂是如何实现的有限时等待了。
第一个问题,为什么要用while来判断isAlive条件而不是if?
第二个问题,如何知道已经超时了?
为什么用while(isAlive())?
可以发现这里面的等待是用的wait(long timeout) 方法实现的,wait方法会使调用join的线程等待。我们知道wait方法除了超时唤醒以外还可以用notify或notifyAll 唤醒。因为现在这个方法的this对象 是我们要等待的线程 t1,假如我在其他地方调用了 t1.nofityAll() 就会使正在等待的线程被唤醒。但是此时t1有可能还没有结束,如果用的是if而不是while就不会回来再次确认isAlive这个条件,导致了假唤醒 。
综上,之所以用while而不是if就是为了避免假唤醒的出现。这其实和wait()、notify()的常规使用类似,我们往往也是用的下面这个模板来实现的wait/notify
while(条件){
wait();
}
如何判断超时?
通过now、base、delay这三个变量实现的超时判断,base表示进入该方法时的时间,now表示距离进入该方法经过了多少时间,delay表示还剩多少时间达到超时的时间。它们三者的表达式如下:
base = System.currentTimeMillis();
now = System.currentTimeMillis() - base;
delay = now - millis;
每一次循环都会计算并判断一次剩余时间,如果剩余时间小于等于零即超时了,就会退出循环结束等待。这里是小于等于零而不是等于零,这是因为代码的执行不是原子的,可能在中间因为CPU调度的问题经过了一些其他线程执行的时间,导致delay值有可能是小于零的。
通过这两者结合就能实现保护性暂停。