美文网首页
Thread的join方法原理

Thread的join方法原理

作者: 编了个程 | 来源:发表于2021-07-15 22:40 被阅读0次

    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对象锁,你说它没释放锁也对,因为从调用线程的角度来看,它并没有释放当前调用线程持有的对象锁。

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

    image.png

    谁唤醒了?

    源码看到这,我又有了一个新的疑问: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来唤醒的。

    求个支持

    我是Yasin,一个坚持技术原创的博主,我的wx公众号是:编了个程

    都看到这儿了,如果觉得我的文章写得还行,不妨支持一下。

    文章会首发到公众号,阅读体验最佳,欢迎大家关注。

    你的每一个转发、关注、点赞、评论都是对我最大的支持!

    还有学习资源、和一线互联网公司内推哦

    求个支持

    我是Yasin,一个坚持技术原创的博主,我的微信公众号是:编了个程

    都看到这儿了,如果觉得我的文章写得还行,不妨支持一下。

    文章会首发到公众号,阅读体验最佳,欢迎大家关注。

    你的每一个转发、关注、点赞、评论都是对我最大的支持!

    还有学习资源、和一线互联网公司内推哦

    相关文章

      网友评论

          本文标题:Thread的join方法原理

          本文链接:https://www.haomeiwen.com/subject/mltvpltx.html