Java中如何让多线程按照自己指定的顺序执行

摘要:基于如何让多线程按照自己指定的顺序执行这个场景,浅谈Thread中join()函数的作用和原理。

join的作用

   之前有人问过我一个这样的面试题:如何让多线程按照自己指定的顺序执行?这个问题最简单的回答是通过Thread.join来实现。

   让父线程等待子线程结束之后才能继续运行。我们来看看在 Java 7 Concurrency Cookbook 中相关的描述(很清楚地说明了 join() 的作用):

Waiting for the finalization of a threadIn some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program. For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.

   黑体部分英文的大意是当我们用某个线程调用这个方法时,join方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。下面用一个示例验证一下:

public class JoinDemo extends Thread {
    int i;
    Thread previousThread; //上一个线程

    public JoinDemo(Thread previousThread, int i) {
        this.previousThread = previousThread;
        this.i = i;
    }

    @Override
    public void run() {
        try {
            //调用上一个线程的join方法,自己演示的时候可以把这行代码注释掉
            previousThread.join();
            Object aa = new Object();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(previousThread.getName() + ", num:" + i);
    }

    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        previousThread.setName("parent thread");
        for (int i = 0; i < 10; i++) {
            JoinDemo joinDemo = new JoinDemo(previousThread, i);
            joinDemo.start();
            previousThread = joinDemo;
            previousThread.setName("child thread " + i);
        }
    }
}

   上面的代码,注意 previousThread.join()部分,大家可以把这行代码注释以后看看运行效果,在没有加join的时候运行的结果是不确定的,加了join以后运行结果按照递增的顺序输出。

源码分析

public class Thread implements Runnable {
    ...
    public final void join() throws InterruptedException {
        join(0);
    }
    ...
    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) { //判断是否携带阻塞的超时时间,等于0表示没有设置超时时间
            while (isAlive()) {//isAlive获取线程状态,无线等待直到previousThread线程结束
                wait(0); //调用Object中的wait方法实现线程的阻塞
            }
        } else { //阻塞直到超时
            while (isAlive()) { 
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    ...

   由此可见,join()方法是通过调用Object中的wait()函数实现线程的阻塞。main线程在调用previousThread.join()时,会持有线程对象previousThread的锁(wait 意味着拿到该对象的锁),然后调用previousThread的wait方法造成主线程阻塞,直到该对象唤醒main线程(即子线程previousThread执行完毕退出的时候)。synchronized修饰在方法层面相当于synchronized(this),this就是previousThread本身的实例。

   子线程结束后,子线程previousThread的this.notifyAll()会被调用,join()返回,主线程只要获取到锁和CPU执行权,就可以继续执行了。

小结

   首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了previousThread.join()方法,相当于在previousThread.join()代码这块写了一个同步代码块,谁去执行了这段代码呢?是主线程,所以主线程被wait()了。然后在子线程previousThread执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有previousThread这个对象锁的线程,也就是主线程,使主线程继续执行。

Thread.join使用场景

   在实际使用过程中,我们可以通过join方法来等待线程执行的结果,其实有点类似future/callable的功能。通过以下伪代码来说明join的使用场景:

public void joinDemo(){
   //....
   Thread t=new Thread(payService);
   t.start();
   //.... 
   //其它业务逻辑处理,不需要确定t线程是否执行完
   insertData();
   //后续的处理,需要依赖t线程的执行结果,可以在这里调用join方法等待t线程的执行结束
   t.join();
}

Reference

posted @ 2021-04-25 20:48  楼兰胡杨  阅读(826)  评论(0编辑  收藏  举报