同步模式之保护性暂停

475 阅读2分钟

保护性暂停(Guarded Suspension)模式是让一个线程等待另一个线程的结果。java中的join、Future、FutureTask均采用了该模式实现。

保护性暂停.png

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方法除了超时唤醒以外还可以用notifynotifyAll 唤醒。因为现在这个方法的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值有可能是小于零的。

通过这两者结合就能实现保护性暂停。