Thread的Join方法原理

开发 前端
众所周知,「Java的锁其实本质上是对象锁」,因为我们前面调用的是thread.join(),所以这里的“锁”对象其实thread这个对象。那这里wait释放的是thread这个对象锁。

[[410940]]

本文转载自微信公众号「编了个程」,作者Yasin x 。转载本文请联系编了个程公众号。

Y说

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

join方法释放锁吗?

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

释放thread对象锁

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

  1. public class Join { 
  2.     static class ThreadA implements Runnable { 
  3.  
  4.         @Override 
  5.         public void run() { 
  6.             try { 
  7.                 System.out.println("我是子线程,我先睡一秒"); 
  8.                 Thread.sleep(1000); 
  9.                 System.out.println("我是子线程,我睡完了一秒"); 
  10.             } catch (InterruptedException e) { 
  11.                 e.printStackTrace(); 
  12.             } 
  13.         } 
  14.     } 
  15.  
  16.     public static void main(String[] args) throws InterruptedException { 
  17.         Thread thread = new Thread(new ThreadA()); 
  18.         thread.start(); 
  19.         thread.join(); 
  20.         System.out.println("如果不加join方法,我会先被打出来,加了就不一样了"); 
  21.     } 

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

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

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

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

  1. if (millis == 0) { 
  2.     while (isAlive()) { 
  3.         wait(0); 
  4.     } 
  5. else { 
  6.     while (isAlive()) { 
  7.         long delay = millis - now; 
  8.         if (delay <= 0) { 
  9.             break; 
  10.         } 
  11.         wait(delay); 
  12.         now = System.currentTimeMillis() - base; 
  13.     } 

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

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

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

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

打印结果:

  1. 我是子线程,我先睡一秒 
  2. 我占住了thread锁 
  3. 我是子线程,我睡完了一秒 
  4. 我thread锁释放了 
  5. 如果不加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.

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

里面有这么一段代码:

  1. if (group != null) { 
  2.     group.threadTerminated(this); 
  3.     group = null
  4.  
  5. void threadTerminated(Thread t) { 
  6.     synchronized (this) { 
  7.         remove(t); 
  8.  
  9.         if (nthreads == 0) { 
  10.             notifyAll(); 
  11.         } 
  12.         if (daemon && (nthreads == 0) && 
  13.             (nUnstartedThreads == 0) && (ngroups == 0)) 
  14.         { 
  15.             destroy(); 
  16.         } 
  17.     } 

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

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

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

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

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

如果线程没启动

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

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

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

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

总结

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

 

责任编辑:武晓燕 来源: 编了个程
相关推荐

2023-05-08 00:08:51

Hive机制场景

2017-08-31 16:17:35

SQL优化器原理

2009-04-02 10:23:13

实现JoinMySQL

2009-06-29 18:08:51

Java多线程join方法

2018-01-25 19:09:40

JavaThreadLocal线程

2009-05-07 15:02:42

OracleJoin查询

2009-08-26 16:58:12

调用C# Thread

2023-06-07 07:43:57

数据库JOIN类型

2023-07-03 08:10:51

2010-05-21 17:30:28

2021-09-10 06:50:03

HashMapHash方法

2011-06-22 15:42:18

QT 信号

2016-12-21 14:35:46

响应式网页布局实现方法原理

2022-03-30 08:54:21

线程 Thread判断线程池任务Java

2021-10-15 11:37:44

反爬虫破解

2013-03-11 10:07:36

主干交换机交换机工作原理交换机设备

2023-08-08 00:06:31

2009-08-04 17:08:12

C# Thread类

2013-05-21 15:03:23

MariaDB

2024-01-09 09:46:13

数据库MySQL
点赞
收藏

51CTO技术栈公众号