Thread的join方法原理

简介: 今天没什么要说的。我个人很喜欢拍天空的照片,放一张前段时间晚上拍的照片吧。

Y说

今天没什么要说的。我个人很喜欢拍天空的照片,放一张前段时间晚上拍的照片吧。

网络异常,图片无法展示
|


join方法释放锁吗?

前段时间,有一个读者私信我,问了这么一个问题:Thread实例的join方法内部是调用的wait方法,而wait方法是会释放锁的,为什么网上很多文章(包括我们之前写的开源书《深入浅出Java多线程》)会说join方法不释放锁?

网络异常,图片无法展示
|


释放thread对象锁

我们先用书中的一个例子说起:

public class Join {
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了一秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
    }
}

在这个例子中,我们在main方法中调用了thread.join(),打印出来的效果就是:

我是子线程,我先睡一秒
我是子线程,我睡完了一秒
如果不加join方法,我会先被打出来,加了就不一样了

这个例子想要表达的意图很简单,就是通过thread实例的join方法,达到main线程等待thread线程执行完后再继续执行的效果。

那join方法底层是如何实现这个功能的呢?究竟会不会释放锁呢?我们点进去看看源码。

if (millis == 0) {
    while (isAlive()) {
        wait(0);
    }
} else {
    while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {
            break;
        }
        wait(delay);
        now = System.currentTimeMillis() - base;
    }
}

可以看到,join的底层是调用的wait(long)方法。而wait方法是Object类型的实例方法,会释放当前Object的锁,且需要拿到当前Object的锁才行。

这么说可能有点绕。众所周知,Java的锁其实本质上是对象锁,因为我们前面调用的是thread.join(),所以这里的“锁”对象其实thread这个对象。那这里wait释放的是thread这个对象锁。

我们把上面的main方法简单改一下,用另一个线程是占住thread这个对象锁,就比较直观了:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new ThreadA());
    thread.start();
    new Thread(() -> {
        // 把thread对象作为锁占住,这样下面的join里面的wait只有等锁释放了才能执行。
        synchronized (thread) {
            try {
                System.out.println("我占住了thread锁");
                Thread.sleep(10000);
                System.out.println("我thread锁释放了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    thread.join();
    System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
}

打印结果:

我是子线程,我先睡一秒
我占住了thread锁
我是子线程,我睡完了一秒
我thread锁释放了
如果不加join方法,我会先被打出来,加了就不一样了

这就印证了那句话:wait方法执行前,是需要获取当前对象的锁的。

所以回归到最开始的问题:join()方法会释放锁吗?严瑾的答案是它会释放thread实例的对象锁,但不会释放其它对象锁(包括main线程)。stackoverflow也对这个有讨论:Does Thread.join() release the lock? Or continue to hold it?。

简单来说,你说它释放了锁也对,因为它确实通过wait方法释放了thread对象锁,你说它没释放锁也对,因为从调用线程的角度来看,它并没有释放当前调用线程持有的对象锁。

当然,为了防止其它读者看到这也有这个疑惑,我直接把文中的这句话删掉了。

网络异常,图片无法展示
|


谁唤醒了?

源码看到这,我又有了一个新的疑问:join方法内部是一个while循环。wait释放了锁,那必然会有一个人来唤醒它,程序才能够继续往下走。那必然有一个地方调用了thread对象的notify方法。

我们在Thread类里面可以找到一个exit()方法,上面备注写着:This method is called by the system to give a Thread a chance to clean up before it actually exits.

这么简单的英文大家应该都能看懂吧?

里面有这么一段代码:

if (group != null) {
    group.threadTerminated(this);
    group = null;
}
void threadTerminated(Thread t) {
    synchronized (this) {
        remove(t);
        if (nthreads == 0) {
            notifyAll();
        }
        if (daemon && (nthreads == 0) &&
            (nUnstartedThreads == 0) && (ngroups == 0))
        {
            destroy();
        }
    }
}

一开始我以为是在这里唤醒的,但仔细一看,这里调用的对象是ThreadGroup的实例,而不是thread实例。所以应该不是这个地方。

经过一通google之后,我又在stackoverflow上找到了正确的答案(stackoverflow, yyds):who and when notify the thread.wait() when thread.join() is called?

答案显示,这是在JVM层面去做的事:

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

可以看到除了notify_all以外,它其实做了很多扫尾的工作。包括处理异常、设置线程状态等。


如果线程没启动

再把代码改一下,如果线程没有通过start启动会怎样呢?

Thread thread = new Thread(new ThreadA());
// thread.start();
thread.join();
System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");

会直接执行最后一行代码打印出来。

看join源码就知道了,在wait之前,会有一个isAlive()的判断,看当前线程是否是alive的。如果没有start,那就会直接返回false,不进入wait。


总结

join方法会释放thread对象锁,底层是wait方法,在JVM层面通过notify_all来唤醒的。


目录
相关文章
Beyond Compare 4密钥过期解决办法,超实用
Beyond Compare 4密钥过期解决办法,超实用
36875 1
|
缓存 Java Spring
Spring循环依赖:当两个Bean陷入鸡生蛋死循环时...
Spring中循环依赖问题常见于Bean相互依赖时,尤其在单例模式下。文章深入解析了循环依赖的成因及Spring的三级缓存解决方案,帮助理解Bean生命周期与依赖管理。
|
6月前
|
缓存 监控 Java
【分布式】分布式核心组件——分布式熔断降级:熔断器状态机、熔断策略、降级方案、Resilience4j/Sentinel实现
本文系统化梳理分布式熔断降级完整知识体系,涵盖核心定位、状态机模型、熔断策略(慢调用/异常比例/数)、降级方案、Resilience4j与Sentinel深度对比、生产落地实践及云原生进阶扩展,助力学习、开发与面试一站式掌握。
|
7月前
|
人工智能 JavaScript Shell
阿里云轻量服务器分钟级部署Openclaw(Clawdbot),接入微信/钉钉/飞书/QQ零图文教程
在AI自动化工具普及的2026年,OpenClaw(Clawdbot)凭借开源免费、功能全面、可扩展性强的优势,成为个人与轻量团队提升效率的核心工具。它能自主完成邮件整理、日程管理、数据抓取、定时任务执行等重复性工作,更可无缝对接微信、钉钉、飞书三大主流通讯平台,实现多端便捷调用。
1162 14
|
Ubuntu 物联网 Linux
从零安装一个Linux操作系统几种方法,以Ubuntu18.04为例
一切就绪后,我们就可以安装操作系统了。当系统通过优盘引导起来之后,我们就可以看到跟虚拟机中一样的安装向导了。之后,大家按照虚拟机中的顺序安装即可。 好了,今天主要介绍了Ubuntu Server版操作系统的安装过程,关于如何使用该操作系统,及操作系统更深层的原理,还请关注本号及相关圈子。
|
8月前
|
缓存 NoSQL Java
若依(RuoYi)框架性能核心:Redis 作用与场景全解析
若依(RuoYi)框架中 Redis 的核心作用、具体应用场景和关键配置逻辑,我会从核心作用分类、实战应用场景、关键配置、优化建议四个维度拆解,覆盖单体版和微服务版的通用逻辑,让你清楚 Redis 在若依中 “做什么、怎么做、怎么优化”
1092 3
|
10月前
|
负载均衡 算法 Java
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
3411 1
|
存储 Java C语言
Java List 复制:浅拷贝与深拷贝方法及区别
我是小假 期待与你的下一次相遇 ~
1359 1
|
存储 Java 数据库连接
MyBatis的类型处理器TypeHandler与自定义实现
MyBatis的类型处理器TypeHandler与自定义实现
1936 0