目录
Java中如何让多线程按照自己指定的顺序执行?
这个问题最简单的回答是通过Thread.join来实现,久而久之就让很多人误以为Thread.join是用来保证线程的顺序性的。
1、join()的使用场景
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。
2、join对于线程的作用
3、join()方法:
join()是Thread类的一个方法
是主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行t.join()之后的代码块。
4、join() 源码分析
public final void join() throws InterruptedException {
join(0);
}
再看join(long millis)方法:
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方法实现是通过wait(小提示:Object 提供的方法)。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
join() 一共有三个重载版本,分别是无参、一个参数、两个参数:
public final void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos) throws InterruptedException;
(1) 三个方法都被final修饰,无法被子类重写。
(2) join(long), join(long, long) 是synchronized method,同步的对象是当前线程实例。
(2) 无参版本和两个参数版本最终都调用了一个参数的版本。
(3) join() 和 join(0) 是等价的,表示一直等下去;join(非0)表示等待一段时间。
从源码可以看到 join(0) 调用了Object.wait(0),其中Object.wait(0) 会一直等待,直到被notify/中断才返回。
while(isAlive())是为了防止子线程伪唤醒(spurious wakeup),只要子线程没有TERMINATED的,父线程就需要继续等下去。
(4) join() 和 sleep() 一样,可以被中断(被中断时,会抛出 InterrupptedException 异常);不同的是,join() 内部调用了 wait(),会出让锁,而 sleep() 会一直保持锁。
5、join使用时注意几点:
1、join与start调用顺序问题
上面的讨论大概知道了join的作用了,那么,如果 join在start前调用,会出现什么后果呢?先看下面的测试结果
class BThread extends Thread {
public BThread() {
super("[BThread] Thread");
};
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start.");
try {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " loop at " + i);
Thread.sleep(1000);
}
System.out.println(threadName + " end.");
} catch (Exception e) {
System.out.println("Exception from " + threadName + ".run");
}
}
}
public class TestDemo {
public static void main(String[] args) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start.");
BThread bt = new BThread();
try {
bt.join();
bt.start();
Thread.sleep(2000);
} catch (Exception e) {
System.out.println("Exception from main");
}
System.out.println(threadName + " end!");
}
}
执行结果:
main start.
[BThread] Thread start.
[BThread] Thread loop at 0
[BThread] Thread loop at 1
main end!
[BThread] Thread loop at 2
[BThread] Thread loop at 3
[BThread] Thread loop at 4
[BThread] Thread end.
main线程没有等待[BThread]执行完再执行。
join方法必须在线程start方法调用之后调用才有意义。
这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。
2、join()与异常
在join()过程中,如果当前线程被中断,则当前线程出现异常。(注意是调用thread.join()的线程被中断才会进入异常,比如a线程调用b.join(),a中断会报异常而b中断不会异常)
如下:threadB中启动threadA,并且调用其方法等待threadA完成,此时向threadB发出中断信号,会进入中断异常代码。
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start.");
BThread bt = new BThread();
// try {
Thread.sleep(2000);
Thread.currentThread().interrupt();
bt.start();
bt.join();
// } catch (Exception e) {
// System.out.println("Exception from main");
// }
System.out.println(threadName + " end!");
}
}
执行结果:
main start.
[BThread] Thread start.
[BThread] Thread loop at 0
Exception in thread "main" java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Thread.join(Thread.java:1249)
at java.lang.Thread.join(Thread.java:1323)
at com.dxz.join.TestDemo.main(TestDemo.java:46)
[BThread] Thread loop at 1
[BThread] Thread loop at 2
[BThread] Thread loop at 3
[BThread] Thread loop at 4
[BThread] Thread end.
6、什么时候会使用Thread.join
在实际应用开发中,我们很少会使用thread.join。在实际使用过程中,我们可以通过join方法来等待线程执行的结果,其实有点类似future/callable的功能。
我们通过以下伪代码来说明join的使用场景、
public void joinDemo(){
//....
Thread t=new Thread(payService);
t.start();
//....
//其他业务逻辑处理,不需要确定t线程是否执行完
insertData();
//后续的处理,需要依赖t线程的执行结果,可以在这里调用join方法等待t线程执行结束
t.join();
}