一、Thread.join()方法的作用
首先,说到 Thread.join()方法,如果我们了解过 java 并发知识的同学可能都知道,我们可以用 Thread.join()方法来控制线程的执行顺序,顾名思义也能知道,join 嘛,加入的意思,也就是让当前正在执行的线程等待,让加入的线程先执行完,然后唤醒当前主线程,再去执行当前主线程。举个例子如下。
public class JoinDemo extends Thread {
Thread previousThread;
public JoinDemo(Thread previousThread) {
this.previousThread = previousThread;
}
@Override
public void run() {
try {
// 调用线程的join方法
previousThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是线程:" + Thread.currentThread().getName() + " ,我要等待线程:"+ previousThread.getName() + "执行完!");
}
public static void main(String[] args) throws Exception {
Thread previousThread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
JoinDemo joinDemo = new JoinDemo(previousThread);
joinDemo.setName("子线程" + i);
joinDemo.start();
previousThread = joinDemo;
}
Thread.sleep(1000);
System.out.println("我现在循环完了,你们可以输出了...");
}
}
输出结果如下:
我现在循环完了,你们可以输出了...
我是线程:子线程0 ,我要等待线程:main执行完!
我是线程:子线程1 ,我要等待线程:子线程0执行完!
我是线程:子线程2 ,我要等待线程:子线程1执行完!
我是线程:子线程3 ,我要等待线程:子线程2执行完!
我是线程:子线程4 ,我要等待线程:子线程3执行完!
我是线程:子线程5 ,我要等待线程:子线程4执行完!
我是线程:子线程6 ,我要等待线程:子线程5执行完!
我是线程:子线程7 ,我要等待线程:子线程6执行完!
我是线程:子线程8 ,我要等待线程:子线程7执行完!
我是线程:子线程9 ,我要等待线程:子线程8执行完!
很明显,Thread.join()能很好的控制线程的执行顺序。
二、Thread.join()方法的原理与深入分析
既然我们知道了 join()方法的作用,那么它到底是如何控制线程的执行顺序的呢,下面一一给你揭晓。
首先,调用完 join()方法,它是如何让当前线程进入等待状态的?
结合上面实例的输出结果,我们来分析这个问题。我们看终端输出的日志中这一行:
我是线程:子线程0 ,我要等待线程:main执行完!
我们在 for 第一层中,首先构建对象 JoinDemo joinDemo = new JoinDemo(previousThread);
传递进去的也就是主线程 -- main 线程。然后调用 start()方法,启动子线程执行。
这个时候,子线程执行 run()方法体的时候,执行到 previousThread.join()
,根据我们对 join()方法的理解,它会让当前线程阻塞,当前线程也就是线程名为”子线程 0“的线程。那么 join()是如何阻塞当前线程(子线程 0)的呢?我们跟进去 join()方法的源码看一看,看看到底有何蹊跷。
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
找到重点,如下几行代码:
while (isAlive()) {
wait(0);
}
也就是说在子线程 0 中执行 previousThread.join()
时候,previousThread 代表的是 main 线程,那么 join()方法的含义也就变成了判断 main 线程是不是活着的,如果是活着的,则调用 wait()方法,来阻塞当前线程,所以当前”子线程 0“也就被阻塞了。wait()方法是如何阻塞”子线程 0“的呢?
我们知道 wait()方法,是 Object 对象的方法,wait()方法的含义是,当前线程 A 中调用 xxx.wait(),会把当前线程 A 加入到等待 xxx 对象锁(JVM 会给每一个对象都分配一把唯一的锁,这把锁是在对象中)的等待队列中去,等待其他线程调用 xxx.notify()或者 xxx.notifyAll()来唤醒在等待队列中的线程。
这里我们顺带说一句,正是因为 xxx.wait()方法,会把当前线程加入到 xxx 对象锁的等待队列中,当前线程在调用 xxx.wait()方法的时候一定是持有 xxx 对象锁的,所以 wait()方法一般放在 synchronized 方法或者 synchronized 代码块中执行。
至此,我们已经知道”子线程 0“在调用完 previousThread.join()
方法后,为何进入等待状态了。同时,我们也知道进入等待状态的线程,需要其他线程调用 notify()或者 notifyAll()来唤醒,我们看 join()方法源码只看到了调用 wait()方法进行了阻塞,没有看到在哪儿调用 notify()方法唤醒等待的线程啊,那么到底是如何唤醒的呢?这里就需要翻看 jdk 的源码了。
在 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);
上面的注释,通知唤醒等待在这个线程对象锁的其他阻塞线程们,这个是在线程终止之后做的事情,我们再跟进 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 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
// 这里是清除native线程,这个操作会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
// 调用notifyAll方法
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
重点关注其中如下部分
// 这里是清除native线程,这个操作会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
// 调用notifyAll方法
lock.notify_all(thread);
也就是说,任何线程在退出时候,都会将 isAlive()置为 false,同时通知唤醒等待在这个线程对象锁上面的其他线程们。
那么,回到我们上面提到的”子线程 0“上面来,我们也就知道了,它在”main“线程的对象锁的等待队列上,在”main“线程执行完退出的时候”子线程 0“也就会被唤醒。
三、说点题外话
止于此,关于 Thread.join()方法的分析完毕。下面说点题外话,写这篇文章的目的是告诉自己,任何在自己学习的过程中,希望自己能深挖一步,知其然,更要知其所以然,才能掌握理解的更加透彻,学习整个 java 的过程中,或者说各种框架、中间件的过程中,要了解的更加深入一些,不要永远只停留在会用的层面。当你多尝试深挖几次之后,会发现很多问题的思路想法都是相通的。这个时候的自己分析问题、解决问题的能力才能得到提升。
知易,行难。
以上,与君共勉。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于