保护性暂停模式(同步模式)
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELg10jC2-1611923969803)(https://note.youdao.com/yws/api/personal/file/C8E2C88DB46C4911A8C53CC6F76E7441?method=download&shareKey=1ce13bad7e616ce6d3a26f4a8051ae51)]
join 源码解析
join(long millis)
:等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程还没有执行结束,则不再等待。
说明:例如在主线程中有个 t 线程,在主线程中调用了 t.join(),那么称 t 为 join 的线程,主线程为被 join 的线程。
join 和 wait 这种是由线程对象调用的就得注意下,其实它们都是调用了 wait 方法,wait 方法必须在 synchronized 块里由加锁的对象调用,否则会抛出异常,sychronized 块一定锁住了一个对象,那么调用 wait 方法的就是锁对象(被 synchronized 锁住的对象,join 里是线程对象),这个时候不要犯糊涂,看调用方法发生是在哪个线程执行体(即看哪个线程获取到的锁,肯定是当前线程),那么当前线程就是 Monitor 的 Owner,此时调用 wait 方法会让当前线程让出 Owner ,当前线程就会被阻塞,并加入到 WaitSet 等待别的竞争到锁的线程调用 notify 唤醒。
源码如下:
// wait方法必须在synchronized块里使用,然后让加锁的对象调用wait方法
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
// 如果小于0 ---> 抛出异常
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 如果等于0 ---> 无限等待直至 join 的线程死亡
if (millis == 0) {
while (isAlive()) { // 注意:这里是检查 join 的线程是否还存活
wait(0); // 这里进入无限等待
}
// 如果大于0 ---> 进入保护性暂停模式
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
首先 join 跟其他的保护性暂停模式不同的地方在于,这里等待的结果是线程是否结束,但其实现方式是一样的,此处的 Guarded Object 是 join 的线程对象,被 join 的线程等待 join 的线程结束的结果,只是恰好这个线程对象就是 Guarded Object。
首先从synchronized
方法讲起,这里很巧妙,巧妙在调用这个synchronized
方法的是 join 的线程对象,被 join 的线程对 join 的 线程对象
加了锁(Guarded Object)。简而言之,当前线程锁住的对象是线程对象,此时线程对象(把它看成一个普通的对象)会关联一个 Monitor(管程),加锁后被 join 的线程会成为这个 Monitor 的 Owner,它只有成为了 Owner 才能运行 join 方法。
我们先讨论millis == 0
的情况,先判断线程对象指向的线程还在运行吗,如果还在运行,就可以调用 wait 使被 join 的线程进入 线程对象的监视器上等待
,这个时候就使得被 join 的线程陷入阻塞,直至 join 的线程死亡,当然这里还有一个隐式的条件。
线程死亡的时候会自动调用自己的 notifyAll 方法,将关联此线程对象的 Monitor 上 WaitSet 中所有的线程唤醒,简而言之就是唤醒所有对自己线程对象加锁的线程中处于
WAITING
状态的线程,这个是由JVM底层实现的,源码中没有,不过我们可以通过分析源码的逻辑看出这个机制,这个机制是实现很多同步方法的基础。所以当 join 的线程执行结束就会唤醒被 join 的线程,这是 wait(0) 的执行流程。
上面这个没什么问题,下面看一下下面的 millis 不等于 0 的情况。下面 while 这段代码的逻辑就是带超时的保护性暂停模式。
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
首先我们知道 wait-notify 机制不太靠谱,因为 notify 是随机唤醒的,如果此时有多个线程都在 WaitSet 里等待,用一个 notify 可能唤醒的并不是我们想要的那个线程(被 join 的线程),为了保证一定可以唤醒那个我们希望的线程,一般我们用 notifyAll
方法。如果别的线程用 notifyAll 就会出现问题了,例如主线程中 join 了一个线程,同时有另外一个线程对 join 的线程对象加锁并处于等待状态,另外的那个线程先结束调用了 notifyAll,而此时并没有到规定的 join 时间,这时主线程就被 虚假唤醒
了,即还没达到唤醒条件就被唤醒。为了让虚假唤醒的线程还能有机会被正确的唤醒,于是我们通常会设计一个 while 循环,然后在被加锁的对象里设置一个标记位,如果被虚假唤醒了不要紧,虚假唤醒后并不会影响标志位,只要 join线程还没死亡它就是 true,还会进入 while 循环只要时间没耗尽还会继续等待,直到标记位为假(线程死亡)才代表被正确唤醒,或者时间耗尽退出等待。
所以上面那个isAlive
方法就是一个标志位,判断当前线程是否处于正在运行(Running
)或就绪(Runnable
)的状态,注意当前线程指的是当前加锁的线程对象指向的线程,所以.isAlive()
方法代表现在加锁的这个线程对象指向的线程是否执行结束,也就是 join 的线程。如果还在运行就一直等待,有两种情况退出等待:
- 标志位为假,即 join 的线程结束,调用 notifyAll,并且标志位为假(这种情况代表正确唤醒);
- 时间耗尽,退出等待。