1. 源码
java
// 源码
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()) {
// 参数为0表示无限期的等待
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
这是Thread.join的核心代码,实际上join方法有重载,分别为join()和join(long millis),在join()方法中实际上执行的是join(0);所以最终都会进入到上述代码块中。
我们知道当调用一个线程对象的join方法时,实际上是让当前线程等待该线程对象执行完成。这里要区分当前的执行线程 和线程对象,这是两个概念
2. 执行过程
先分析下参数millis为0的情况,当参数为0时,我们只需要关注
java
if (millis == 0) {
while (isAlive()) {
// 参数为0表示无限期的等待
wait(0);
}
}
这段代码即可,可以看到使用while循环判断isAlive()的结果,当结果为真时执行wait方法,首先说一下,isAlive是Thread的一个方法
java
// 测试此线程对象是否处于活动状态,如果此线程已经开始运行且尚未终止则属于活动状态
public final native boolean isAlive();
注意这里的线程对象,指的可不是执行线程,是this对象,比如你调用thread.join(),那么isAlive指的就是thread对象,即使执行到该处的线程实际上可能是其他线程。
那么继续往下走,当判断线程处于活动状态时,触发wait()方法,该方法使当前执行线程在this对象上等待并释放this对象锁,那么到此则执行线程就进入等待了,根据join方法的注释得知,当this线程执行完毕时JVM会调用this线程对象上的notifyAll()方法唤醒该对象上的所有等待线程,所以执行线程此刻实际上是在等待this线程执行完成并通知它。
当this对象执行完成后执行线程会从等待集中被唤醒,然后尝试重新获取this锁,获取锁之后再次进行判断时发现isAlive()的结果已经不是true了,说明this线程执行完毕了,那么到此刻结束join方法。
3. 带时间参数的执行过程
java
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()) {
// 参数为0表示无限期的等待
wait(0);
}
} else {
while (isAlive()) {
// 假定第一次进入循环先将入参减去0直接获得需要等待的时长,实际上就是millis,后续再次进入now的值会发生变化
long delay = millis - now;
// 如果≤0则跳出,表示不需要等待了
if (delay <= 0) {
break;
}
// 直接将执行线程等待指定时长 超时会被JVM自动唤醒
wait(delay);
// 计算已经等待了多久 now实际上就是计算执行线程等待的时长
now = System.currentTimeMillis() - base;
}
}
}
其实这里带的时间参数只是让执行线程等待指定时间,如果超时则执行线程也不再等待继续向下执行而已,在上述代码中可能有小伙伴疑惑为啥最后还要计算now的等待时长,wait(delay)方法不是指定了等待时间的吗,被唤醒那肯定是到时间了,now肯定等于delay啊,直接继续往下走就可以了。实际上可能存在欺骗性唤醒 、其他线程调用this上的notifyAll()方法导致执行线程提前被唤醒,这是不确定的,所以我们不能假定执行线程唤醒就是到了规定的等待时间,必须进行再次判断。其他过程和上述不带参的流程基本一致,不再赘述。
4. 示例代码
java
public class JoinExample {
public static void main(String[] args) {
Thread.currentThread().setName("主线程");
Thread t1 = new Thread(() -> {
Tools.randomPause(1000);
System.out.println("我是线程t1");
});
t1.setName("t1");
t1.start();
Thread t2 = new Thread(() -> {
Tools.randomPause(1000);
System.out.println("我是线程t2");
});
t2.setName("t2");
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
这里我修改了main线程的名称,并且新建了两个线程,现在启动这两个线程并且使main线程等待t1和t2执行完成,我在join方法入口、t1和t2执行处打断点,等下我们看下详细信息
进入join方法,我们可以看到当前执行线程是主线程,也就是main线程,this是t2,所以这里isAlive()验证的是t2是否处于活动状态,如果需要等待则main线程在t2实例(因为当前对象this是t2)上进入等待状态