【并发基础】join()方法底层原理详解
创始人
2025-05-29 06:39:51

目录

一、简介

1.1 作用

1.2 join()方法和join(long timeout)方法的使用

二、方法源码

2.1 无参数方法

2.2 有参数方法

注意点一:join阻塞的是当前线程,并不是join方法的线程对象对应的线程

注意点二:唤醒当前线程的操作是在JVM底层实现的,并没有显式调用notifyAll()方法

注意点三:当前线程A在进入到线程对象B的join方法中使获取了线程对象B的锁,在join内部调用wait()方法时又会释放掉线程对象B的锁,在线程B执行完后当前线程才会再次获取线程对象B的锁

注意点四:线程A调用线程对象B的join方法时,只有当此时线程对象已经被启动并且还没有执行完时才会起作用

三、总结

join和wait方法的区别是:


一、简介

1.1 作用

join()方法是Thread类中的一个方法,可以由线程对象调用。在线程操作中,可以使用join()方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行,即等待线程执行终止。在当前线程调用join()方法时并不需要提前自己写同步代码块来获取对象锁,调用了join()方法也不会释放当前线程所持有的对象锁。因为join方法在源码层面就是一个被synchronized修饰的同步方法,所以会进入到方法时会获取到对象锁的,具体在后面的源码中会讲解。

适用场景:需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。

作用:

  • 当线程任务量大时,保证 main 线程在这些线程运行结束后再结束、
  • 可控制子线程间执行顺序

Java中如何让多线程按照自己指定的顺序执行?

这个问题最简单的回答是通过Thread.join来实现,Thread.join的作用之一是用来保证线程的顺序性的。下面这段代码演示了Thread.join的作用:

public class JoinDemo extends Thread{int i;Thread previousThread; //上一个线程public JoinDemo(Thread previousThread,int i){this.previousThread=previousThread;this.i=i;}@Overridepublic void run() {try {//调用上一个线程的join方法,大家可以自己演示的时候可以把这行代码注释掉previousThread.join(); } catch (InterruptedException e) {e.printStackTrace();}System.out.println("num:"+i);}public static void main(String[] args) {Thread previousThread=Thread.currentThread();for(int i=0;i<10;i++){JoinDemo joinDemo=new JoinDemo(previousThread,i);joinDemo.start();previousThread=joinDemo;}}
}

上面的代码,注意 previousThread.join部分,大家可以把这行代码注释以后看看运行效果,在没有加join的时候运行的结果是不确定的。加了join以后,运行结果按照递增的顺序展示出来。

thread.join的含义是当前线程需要等待previousThread线程终止之后才从thread.join返回。简单来说,就是线程没有执行完之前,会一直阻塞在join方法处。

再来一个例子:

在main方法中,创建启动了三个子线程,也就是说这三个线程是main线程的子线程,如果就是正常的启动这三个线程,那么这三个线程的执行顺序是不固定的。但是如果在每一个启动线程之间加上join()方法,如下:

main{thread1.start();thread1.join();thread2.start();thread2.join();thread3.start();thread3.join();
}

这个时候三个线程的执行顺序就固定了,变成了1,2,3.只有当调用join的线程执行完,主线程才会被唤醒继续向下执行,也就达到了控制线程执行顺序的效果。

下面的图表现了join对于线程的作用:

1.2 join()方法和join(long timeout)方法的使用

例子:

public static void main(String[] args){...thread1.join();thread2.join();System.out.println("all child thread over!");
}

主线程首先会在调用thread1.join() 后被阻塞,等待thread1执行完毕后,调用thread2.join(),等待thread2 执行完毕(有可能),以此类推,最终会等所有子线程都结束后main函数才会返回。如果其他线程调用了被阻塞线程的 interrupt() 方法,被阻塞线程会抛出 InterruptedException 异常而返回。

通过上面的例子,我们就能知道join()方法是线程对象调用的,哪个线程对象调用这个方法,就会让哪个线程强制执行,也就是除了这个调用join方法的线程对象对应的线程以外,其他线程都被阻塞,等待这个被强制执行的线程执行完毕后,其他线程才能继续向下执行。

join()方法还提供可以传入时间参数的方法,即指定的等待时间内被join的线程还没执行完,就不再等待,继续向下执行。带参和不带参数方法的区别在于等待方式的不同:

  • 当调用join()方法时,当前线程会被阻塞,进入到WAITING状态join方法就是在当前线程的环境下被另一个线程对象调用的),直到调用join方法的线程对象对应的线程执行完毕后才会继续执行。如果调用join方法的线程对象对应的线程已经执行完毕,那么当前线程会立即继续执行。
  • 当调用join(long timeout)方法时,当前线程也会被阻塞,进入到TIMED_WAITING状态,但是最多只会等待指定的时间(以毫秒为单位),如果等待的时间超过了指定的时间或者调用join方法的线程对象对应的线程已经执行完毕,那么当前线程会立即继续执行。

这里要注意,虽然是通过一个线程对象调用的join方法,但是这个join方法还是在当前线程的环境下调用的,所以其实调用join方法的线程还是当前线程,并不是那个线程对象的线程调用的join方法。

给出一个实例帮助理解:

public class JoinExample {private static final int TIMES = 100;private class JoinThread extends Thread {JoinThread(String name){super(name);}@Overridepublic void run() {for (int i = 0; i < TIMES; i++) {System.out.println(getName() + " " + i);}}}public static void main(String[] args) {JoinExample example = new JoinExample();example.test();}private void test() {for (int i = 0; i < TIMES; i++) {if (i == 20) {Thread jt1 = new JoinThread("子线程1");Thread jt2 = new JoinThread("子线程2");jt1.start();jt2.start();// main 线程调用了jt1线程和jt2线程的join()方法// main 线程必须等到 jt1和jt2线程 执行完之后才会向下执行try {// 需要等到jt1执行完成之后,才向下执行。在jt1没有执行完期间,其他线程无法运行。jt1.join();// 需要等到jt2执行完成之后,才向下执行。在jt2没有执行完期间,其他线程无法运行。jt2.join();// join还存在可以传入时间参数的方法:join(long mills) - 等待时间内被join的线程还没执行,就不再等待,继续向下执行} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "  " + i);}}
}

二、方法源码

上面的例子中主线程是如何被阻塞的?又是通过什么方法唤醒的呢?下面我们就通过源码来看看Thread.join方法做了什么事情。

在底层源码不管是无参数,还是有参数的join都是使用的同一个带参源码方法。

2.1 无参数方法

// 无参数join()方法
public final void join() throws InterruptedException {// 不指定等待时间,就传入0,就会一直等待调用的线程执行完后才会继续向下执行join(0);
}

2.2 有参数方法

join源码底层本质是利用当前线程的线程对象的isAlive()方法和wait()方法实现的。源码如下:

// join方法向外抛出了异常,所以使用join方法需要在外层处理异常或者继续向上抛出异常
// 我们可以看到这个方法是被synchronized修饰的方法,假设当前线程是A,线程A执行了线程对象B的join方法,
// 那么线程A进入到join方法中,就会获取到对象B的对象锁,之所以要用synchronized修饰join方法,就是为了获取B的对象锁,
// 因为join方法底层是利用wait方法实现的,而调用某个对象的wait方法需要持有该对象的锁才行
// 下面的所有讲解就是按照假设当前线程是A,线程A执行了线程对象B的join方法的前提下讲的,此时join方法就是在线程对象B中
public final synchronized void join(long millis)throws InterruptedException {// 下面整个代码环境都是在对象B中的,所以里面直接调用的方法按照Java语法规则,只要没有指定方法所在的对象,那么就都是调用的当前所在对象中的方法,也就是线程对象B中的方法// 获取当前时间long base = System.currentTimeMillis();long now = 0;// 传参小于0,非法参数,抛出异常if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}// 传入0超时时间意味着永远等待,直到B线程执行完毕if (millis == 0) {// 这个isAlive()是线程对象B中的方法// isAlive()方法检查的是调用该方法的线程对象对应的线程是否还在运行,也就是检查线程B(被等待线程)是否在还在运行while (isAlive()) {// 如果B线程还在执行,则当前线程A会调用wait()方法使自己阻塞(等待),直到被等待的B线程执行结束当前线程才会继续执行。// 下面这个wait放大,是线程对象B的wait方法,当前线程A执行了对象B的wait方法,使得当前线程阻塞等待。因为join方法被synchronized修饰,所以当前线程已经持有了对象B的锁了,才能调用B对象的wait方法// 传入0也表示没有等待期限,只有当某个线程调用了B对象的notify()/notifyAll()方法,才有可能会使在对象B上的等待队列中等待的线程A唤醒wait(0);}// 传入的超时时间不为0,意味着如果当前线程A等待了超过millis毫秒了,线程对象B对应的线程还没有执行完,那么也会自动被唤醒继续向下执行,不会一直等待了  } else {// 最多等待 millis 毫秒// isAlive()判断线程B是否执行完成while (isAlive()) {// 每一轮循环都会重新计算还剩下多少等待时间,用最多的等待时间减去当前的时间long delay = millis - now;// 如果delay小于等于0了,说明已经到了等待时间了,这个时候不管线程B是否执行完了,都直接跳出循环,后续会去唤醒当前线程Aif (delay <= 0) {break;}// 如果此时还没有超时并且线程B没有执行完,那么就当前线程A就继续调用线程对象B的wait方法,并且传入最多还要等待的时间,来使当前线程阻塞指定的时间wait(delay);// 每一轮循环都会获取一次当前的时间now = System.currentTimeMillis() - base;}}// join方法执行到最后,JVM底层会去执行让当前线程A唤醒的操作,源码中并没有显式调用notify()/notifyAll()方法,整个唤醒操作是在JVM底层实现的
}

join()方法是用于让一个线程等待另一个线程执行完毕的方法。当一个线程A执行了另一个线程B的join()方法后,线程A将会被挂起,直到线程B执行完毕。

join()方法的底层实现原理是基于对象的wait()和notify()方法来实现的。当一个线程A执行另一个线程B的join()方法时,线程A会进入等待状态(WAITING),线程B会运行直到执行完毕。当线程B执行完毕后,JVM底层会自动调用对象的notifyAll()方法来通知所有等待在该对象上的线程,包括线程A。此时,线程A会重新进入就绪状态(在Java线程中其实就是进入到RUNNABLE状态),等待获取CPU资源继续向下执行。

注意点一:join阻塞的是当前线程,并不是join方法的线程对象对应的线程

有很多人不理解join为什么阻塞的是当前线程,而不是调用join方法的线程对象对应的线程呢?不理解的原因是阻塞当前线程A的wait()方法是放在线程对象B这个实例中被调用的,让大家误以为应该阻塞B线程。但实际上当前线程会持有线程对象B的对象的锁(因为join方法使用synchronized修饰的,所以当前线程也就获取了线程对象B的锁),在线程对象B调用的wait()方法时,而这个wait()方法的调用者线程对象B是在当前线程环境中的。所以造成当前线程阻塞。

注意点二:唤醒当前线程的操作是在JVM底层实现的,并没有显式调用notifyAll()方法

为什么线程B执行完毕就能够唤醒当前线程呢?或者说是在什么时候唤醒的?

这里我们要注意一点,被等待的线程并不会真正地调用notifyAll()方法来唤醒其他等待线程,而是由底层的JVM代码实现自动唤醒等待线程的功能。这个功能在底层被称为“monitor enter”和“monitor exit”,是由JVM来负责管理的。具体实现细节比较复杂,但是对于Java开发者来说,只需要知道在使用join()方法等待线程执行完毕时,等待的线程会被自动唤醒,不需要手动调用notify()或notifyAll()方法。在Thread类的join()方法的源码中,没有直接调用notify()或notifyAll()方法的代码。但是,join()方法的底层实现确实是基于对象的wait()和notify()方法来实现的。

如果想要知道实现唤醒的具体细节,我们就得翻jdk的源码,但是如果大家对线程有一定的基本了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕以后会有一个唤醒的操作,只不过并不是显式调用,而是在JVM底层代码实现的。

接下来在hotspot的源码中找到 thread.cpp,看看被等待线程执行结束以后有没有做相关的事情来证明我们的猜想

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {assert(this == JavaThread::current(),  "thread consistency check");...// Notify waiters on thread object. This has to be done after exit() is called// on the thread (if the thread is the last thread in a daemon ThreadGroup the// group should have the destroyed bit set before waiters are notified).ensure_join(this); assert(!this->has_pending_exception(), "ensure_join should have cleared");...
}

观察一下 ensure_join(this)这行代码上的注释,唤醒处于等待的线程对象,这个是在线程终止之后做的清理工作,这个方法的定义代码片段如下:

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 anywaythread->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//这里是清除native线程,这个操作会导致isAlive()方法返回falsejava_lang_Thread::set_thread(threadObj(), NULL);lock.notify_all(thread);//注意这里// Ignore pending exception (ThreadDeath), since we are exiting anywaythread->clear_pending_exception();
}

ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞的主线程会被唤醒;

也就是说当子线程执行完run方法之后,底层在jvm源码里,会自动执行线程的exit方法,里面会调用notifyAll方法,唤醒所有的线程,而这其中就包含了被阻塞的主线程。

注意点三:当前线程A在进入到线程对象Bjoin方法中使获取了线程对象B的锁,在join内部调用wait()方法时又会释放掉线程对象B的锁,在线程B执行完后当前线程才会再次获取线程对象B的锁

在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的对象时,它必须先获得该锁的所有权。只有获得锁的线程才能调用wait()、notify()和notifyAll()方法。因此,在join()方法中,使用了synchronized关键字来使当前线程获取到Thread对象的锁,确保只有一个线程可以进入join()方法,避免出现竞态条件。

当前线程A调用线程对象B的join方法时,当前线程会尝试获取线程对象B的锁,获取对象B的锁成功后,就会在join方法内部会调用对象B的wait方法,此时当前线程A就会释放线程对象B的锁,使得线程A进入等待状态。此时线程B就会又获取到线程对象B的锁,当线程对象B执行完毕后,JVM底层会调用notifyAll方法唤醒所有等待线程,包括线程A,底层调用了唤醒方法后,线程B又会释放掉线程对象B的锁,此时线程A会重新获取线程对象B的锁(因为是线程A调用的线程对象B的join方法,join方法又是被synchronized修饰的方法,如果线程A唤醒后不再次持有对象B的锁,就没有办法继续在join方法内部继续向下执行了),然后继续执行后面的代码。

注意点四:线程A调用线程对象Bjoin方法时,只有当此时线程对象已经被启动并且还没有执行完时才会起作用

当一个线程对象被创建后,调用该线程对象的start()方法可以启动该线程,线程开始执行。而当一个线程在执行过程中调用另一个线程对象的join()方法时,会让该线程执行完毕后再继续执行当前线程。

start()方法会创建一个新的线程,并且让这个新线程执行run()方法中的代码。如果没有调用start()方法,那么线程对象B只是一个普通的对象,并没有对应的线程。如果线程对象还没有执行start()方法,那么在当前线程中调用该线程对象的join()方法不会有任何效果,因为线程还没有开始执行。这种情况下当前线程调用另一个线程对象的join()方法会使当前线程进入等待状态,直到被等待的线程结束或者超时,但是被等待的线程并没有开始执行,甚至都不存在这个线程,所以当前线程会一直处于等待状态,直到超时或者被中断。

还有一点需要注意的是,当线程A执行join()方法等待线程B执行完毕时,如果线程B已经执行完毕了,那么线程A并不会阻塞,而是直接退出join()方法,继续执行下面的代码。

所以,在使用join()方法之前,一定要确保调用了start()方法来启动相应的线程。

以上join()方法的简单实现原理。实际上,Java虚拟机在实现中还考虑了许多细节,以确保线程的正确协作和优化执行效率。

总结:

  • 当一个线程对象调用了join方法后,它就会让当前线程(即调用join方法的线程)进入等待状态,直到这个线程对象对应的线程执行完毕为止。
  • join方法的底层实现原理是基于对象的wait和notify方法来实现的。在当前线程的环境下当线程对象调用了join方法时,当前线程会获取这个对象的锁,并且调用这个对象的wait方法来使当前线程阻塞等待。当这个对象对应的线程执行完毕后,JVM底层会调用notifyAll方法来唤醒所有等待在这个对象上的线程,包括当前线程。这个唤醒过程是在JVM底层实现的,作为用户的我们看不到显式的调用唤醒方法的代码。
  • 源码中有相关的体现,可以参考Thread类中join和isAlive方法以及JVM中ensure_join和lock.notify_all方法(就是JVM中的这两个方法实现了join方法中唤醒当前线程的操作)。

三、总结

Thread.join其实底层是通过wait和notifyall来实现线程的通信达到线程阻塞的目的;

joinwait方法的区别是:

  • wait方法会让当前线程释放对象锁,并进入等待状态,直到被其他线程唤醒或者超时时间到达。
  • join方法不会让当前线程释放对象锁,而是让当前线程进入到等待状态等待目标线程执行完毕或者超时时间到达。

  相关文章:【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
                  【并发基础】Java中线程的创建和运行以及相关源码分析           
                  【并发基础】线程,进程,协程的详细解释
                 【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解
                 【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

上一篇:Java操作Zookeeper节点

下一篇:c指针(二)

相关内容

热门资讯

计算机网络(第九弹) --- ...   传输控制协议 TCP 在整个计算机网络中占有很高的地位, 它会控制着网络上数据的传输过程, 当然...
Java二叉树的前中后序遍历 Java二叉树的前中后序遍历1.前序遍历1.1前序遍历概念1.2前序遍历习题2.中序遍历2.1中序遍...
电动汽车十大名牌排名及价格,纯... 今天给各位分享电动汽车十大名牌排名及价格,纯电动汽车排名及价格...的知识,其中也会对电动汽车十大名...
长安奔奔mini保养(长安奔奔... 本篇文章极速百科给大家谈谈长安奔奔mini保养,以及长安奔奔mini保养手册对应的知识点,希望对各位...
Python-06:异常、模块... 文章目录一、异常1.1 异常的概念1.2 捕获异常的语法1.3 代码演示1.4 异常的传递性二、模块...
满州是哪里(日本口中的满洲是哪... 今天给各位分享满州是哪里的知识,其中也会对日本口中的满洲是哪里进行解释,如果能碰巧解决你现在面临的问...
义乌交通违章查询,浙江义乌交通... 今天给各位分享义乌交通违章查询,浙江义乌交通违章查询的知识,其中也会对义乌违章查询入口进行解释,如果...
WEB安全 DIV CSS基础 1.DIV和CSS样式             层叠样式表(英文全称:Cascadin...
灵感来自游艇?聊天津一汽骏派C... 今天给各位分享灵感来自游艇?聊天津一汽骏派CX65设计的知识,其中也会对一汽骏派suv进行解释,如果...
汽车维修哪个学校比较好?(学汽... 今天给各位分享汽车维修哪个学校比较好?的知识,其中也会对学汽车维修哪个学校好,快来看看!进行解释,如...
科学技术的两面性是什么(科学技... 本篇文章极速百科给大家谈谈科学技术的两面性是什么,以及科学技术的两面性发言稿50字对应的知识点,希望...
京沪高速实时路况(京沪高速实时... 本篇文章极速百科给大家谈谈京沪高速实时路况,以及京沪高速实时路况今天封闭没有对应的知识点,希望对各位...
[刷题 java版] | 字节... 1.万万没想到之聪明的编辑我叫王大锤,是一家出版社的编辑。我负责校对投稿来的英文稿件&...
#科研筑基# 吴恩达深度学习 ... 引例这门课的名字叫深度学习,为什么我们要先介绍神经网络呢?那是因为&#x...
vue3一天内快速学习 文章目录简介vue3 学习安装项目结构基础知识模版语法{{}}v-htmlv-bind渲染展示v-i...
特斯拉专题研究报告:特斯拉第三... 今天给各位分享特斯拉专题研究报告:特斯拉第三篇章展望的知识,其中也会对特斯拉研究成果进行解释,如果能...
高速免费提前几个小时(高速免费... 今天给各位分享高速免费提前几个小时的知识,其中也会对高速免费提前几个小时上高速进行解释,如果能碰巧解...
卡罗拉和朗逸怎么选哪个更值得入... 今天给各位分享卡罗拉和朗逸怎么选哪个更值得入手的知识,其中也会对卡罗拉和朗逸买哪个好更好进行解释,如...
MAC QT OpenGL 图... 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES...
海藻粉是什么东西(海藻粉是什么... 今天给各位分享海藻粉是什么东西的知识,其中也会对海藻粉是什么东西 宝宝能吃吗进行解释,如果能碰巧解决...
关于MGRE的OSPF建邻 我们以实验的方式直观的看一下OSPF在MGRE中的建邻。 首先介绍一下接口网络类型。 MGRE嘴上形...
MySQL和MariaDB,它... 目录 一、MySQL简介 二、MariaDB简介 三、什么是MariaDB? 四、为什么推出Mari...
羡慕的意思是什么(羡慕的意思是... 本篇文章极速百科给大家谈谈羡慕的意思是什么,以及羡慕的意思是什么最佳答案对应的知识点,希望对各位有所...
发蜡有什么作用(发蜡有什么作用... 今天给各位分享发蜡有什么作用的知识,其中也会对发蜡有什么作用和用途进行解释,如果能碰巧解决你现在面临...
欧洲杯几月开始几月结束(欧洲杯... 今天给各位分享欧洲杯几月开始几月结束的知识,其中也会对欧洲杯啥时候开始啥时候结束进行解释,如果能碰巧...
横滨轮胎型号及价格表(横滨轮胎... 今天给各位分享横滨轮胎型号及价格表的知识,其中也会对横滨轮胎型号性能介绍进行解释,如果能碰巧解决你现...
基于深度学习的跌倒检测系统(U... 摘要:跌倒监测系统用于智能化监测是否有行人跌倒,通过YOLOv5的深度学...
基于STM32 + FPGA ... 针对在软体机器人控制时,多电机协同控制过程中难度大、通用性差、协同性差等缺点ÿ...
【Docker】什么是Dock... 文章目录Docker出现的背景解决问题docker理念容器与虚拟机比较容器发展简史传统虚拟机技术容器...
五十万以内买SUV,这几款的第... 本篇文章极速百科给大家谈谈五十万以内买SUV,这几款的第三排都能坐成年人,以及五十万左右七座suv对...