【Java多线程】——join()方法

在一些情况下,主线程创建了子线程并启动,主线程和子线程异步执行。但是主线程可能会需要子线程的一些结果来执行接下来的任务,这时就需要等待自线程先执行完再继续执行主线程。我们可能会想到使用同步(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(),则可以输出正确结果。


猜你喜欢

转载自blog.csdn.net/u012198209/article/details/80336432