深入解析join源码和保护性暂停模式

本文深入探讨了保护性暂停模式(Guarded Suspension)在多线程编程中的应用,特别是通过JDK中的join和Future实现。文章详细分析了join方法的源码,解释了其如何在同步模式下工作,以及线程间如何通过GuardedObject进行结果传递。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

保护性暂停模式(同步模式)

即 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,并且标志位为假(这种情况代表正确唤醒);
  • 时间耗尽,退出等待。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值