在一些情况下,主线程创建了子线程并启动,主线程和子线程异步执行。但是主线程可能会需要子线程的一些结果来执行接下来的任务,这时就需要等待自线程先执行完再继续执行主线程。我们可能会想到使用同步(synchronized)的方法,使主线程和子线程共同竞争一个对象锁,这样就可以达到依次同步执行的目的了。这个方法虽然可行,但是看上去可能麻烦了一些,这里就引入了一个概念叫做join()方法。
一、join()的用法
我们首先来看一下join()的用法:
public class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"线程执行,这是首先要执行的任务...");
Thread.sleep(2000);
System.out.println("执行完毕,可以继续执行主线程...");
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String args[]) {
try {
MyThread mt = new MyThread();
System.out.println(Thread.currentThread().getName()+"线程开始执行...需要等待子线程完成任务");
mt.start();
mt.join();
System.out.println(Thread.currentThread().getName()+"线程继续执行...");
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
运行程序,输出以下结果:
/*
main线程开始执行...需要等待子线程完成任务
Thread-0线程执行,这是首先要执行的任务...
执行完毕,可以继续执行主线程...
main线程继续执行...
*/
可以看到,在主程序执行到启动子线程mt后,调用了mt.join()方法,于是主线程开始等待,直到子线程mt的run()方法执行完毕,线程销毁以后,才继续执行主线程的任务。
二、join()内部通过wait()实现等待
以上程序成功地完成了我们需要的执行顺序,即主线程等待子线程执行完毕再继续执行自己的任务,那么join()方法内部是如何实现的呢?我们来看一下join()的JDK源码:
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;
}
}
}
上面是join(long)的源码。可以看到,在join()方法内部,是通过wait方法来实现线程的等待的。具体一点说,join方法是一个synchronized同步方法,所以在调用join方法时,首先要获得调用此方法的实例对象的对象锁。join()方法默认返回join(0),当调用join(millis)时,程序用0初始化now变量,计算now和millis的差值赋值给delay,然后程序调用wait(delay),线程释放当前对象的锁,等待唤醒;如果当前线程在millis时间内被唤醒,则比较后继续进入wait状态同时更新now的值;如果当前线程超出millis时间后才被唤醒则直接break,继续执行join下面的代码。
通过上面的描述可以知道,拿上面的例子来说明,当主线程执行join()时,主线程迅速获得mt对象的锁,然后执行wait()后立即释放,同时mt线程继续执行,而主线程反复调用wait直到mt执行完毕,主线程继续执行join后面的代码。
join执行后,如果当前线程被中断,则当前线程会抛出InterruptException异常,但是调用join()方法的线程对象如果还在执行,则不会受到影响,会继续执行下去。
ps:有一点要注意的是,join(long)和sleep(long)不同的地方是,sleep时线程进入睡眠但不释放对象锁,而join由于通过wait实现,所以要释放对象锁。
三、join(long)意外:join后的代码先执行
使用join(long)的时候要注意,有时可能会出现意外的运行结果,如下以下例子:
public class ThA extends Thread{
@Override
public synchronized void run() {
try {
System.out.println("线程"+Thread.currentThread().getName()+"开始运行,"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕,"+System.currentTimeMillis());
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
//线程B的run()方法需要获得thA的锁才能执行
public class ThB extends Thread{
private ThA thA;
public ThB(ThA thA) {
super();
this.thA = thA;
}
@Override
public void run() {
try {
synchronized(thA) {
System.out.println("线程"+Thread.currentThread().getName()+"开始运行,"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕,"+System.currentTimeMillis());
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String args[]) {
try {
ThA thA = new ThA();
ThB thB = new ThB(thA);
thA.setName("A");
thB.setName("B");
thA.start();
thB.start();
thA.join(1000);
System.out.println("main 线程 结束 !");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
执行结果如下:
/*
线程A开始运行,1526563262236
线程A执行完毕,1526563265242
main 线程 结束 !
线程B开始运行,1526563265242
线程B执行完毕,1526563268246
*/
显然这不是我们想要的结果,思考一下为什么会出现这样的结果呢?
1)首先启动两个线程之后,join(1000)会迅速抢占thA的锁,然后立即执行wait(1000)释放锁;
2)接着thA线程抢到自己的对象锁,接着执行同步run()方法,期间经过3000ms,然后释放thA的对象锁;
3)join(1000)又一次抢到了thA的对象锁,但是发现1000ms已经超时,所以计算出delay<0,直接break并释放thA的锁,然后执行join(1000)下面的代码,输出“main 线程 结束 !”;
4)此时线程B获得了thA的对象锁,输出相关内容。
可以看到,由于join(millis)中millis的值设置不当,导致join()后面的代码先执行,出现意外。我们只要将join(1000)改为join(),则可以输出正确结果。