多线程中的wait与join

本来是想把标题定为《Thread的wait与join》,后来想想不严谨,因为wait是Object的方法,不是Thread独有的,所以这里要注意一下。
关于wait()方法,在Object中有三个重载方法:

    public final native void wait(long timeout) throws InterruptedException;

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

    public final void wait() throws InterruptedException {
        wait(0);
    }

无论是哪个,最终都是要调用本地方法 wait(long timeout) 。
开局一张图:


image.png

今天主要就来讲一下wait(),join()的关系吧。

wait

一个Object的方法,目的是将调用obj.wait()的线程置为waiting的状态,等待其他线程调用obj.notify()或者obj.notifyAll()来唤醒。最常见的就是生产者/消费者功能。

有一点注意的就是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会抛出IllegalMonitorStateException异常。

wait/notify的通俗解释:
1.线程A首先获取到obj的锁,然后执行了obj.wait(),这个方法就会是线程A暂时让出对obj锁的持有,并把线程A转换waiting状态,同时加入锁对象的等待队列。
2.线程B获取到了obj的锁,然后执行了obj.notify(),这个方法通知了锁对象的等待队列,使正在等待队列中的线程A改为阻塞状态,使A进入对obj锁的竞争。当然在执行notify后并不会使线程A马上获取到锁,因为线程B目前还在持有obj的锁。
3.线程A获取到obj锁,继续从wait()之后的代码运行。

举个例子:

public class ThreadTest {

    static final Object obj = new Object();  //对象锁

    private static boolean flag = false;

    public static void main(String[] args) throws Exception {

        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();

        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 生产者线程
    static class Produce implements Runnable {

        @Override
        public void run() {

            synchronized (obj) {
                System.out.println("进入生产者线程");
                System.out.println("生产");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  //模拟生产过程
                    flag = true;
                    obj.notify();  //通知消费者
                    TimeUnit.MILLISECONDS.sleep(1000);  //模拟其他耗时操作
                    System.out.println("退出生产者线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消费者线程
    static class Consume implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("进入消费者线程");
                System.out.println("wait flag 1:" + flag);
                while (!flag) {  //判断条件是否满足,若不满足则等待
                    try {
                        System.out.println("还没生产,进入等待");
                        obj.wait();
                        System.out.println("结束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println("消费");
                System.out.println("退出消费者线程");
            }

        }
    }
}

结果:

进入消费者线程
wait flag 1:false
还没生产,进入等待
进入生产者线程
生产 // 1秒后notify
退出生产者线程 //生产线程结束
结束等待
wait flag 2:true
消费
退出消费者线程

可以看到,生产线程在notify后,消费线程并没有马上继续运行,原因就是上面提到的第二点。

wait()方法有了一个大概的了解,下面看看join的原理。

join

join方法通常的解释就是等待调用线程执行完毕后,再继续执行当前线程。通常用在多线程的业务上,某个线程的运算需要另一个线程的结果时,就可以使用join。

来个例子:

public class JoinMainDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 1");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 2");
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 3");
            }
        });

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();

        System.out.println("thread Main");
    }
}

情况1输出:

thread 1
thread 2
thread 3
thread Main

流程:Main线程调用thread1join后,会等待join执行完毕才会继续运行,thread2,thread3都是这样。1,2,3顺序是固定的。

我们改一下代码顺序

        thread1.start();
        thread3.start();
        thread2.start();
        thread1.join();
        thread2.join();
        thread3.join();

情况2输出:

thread 3
thread 1
thread 2
thread Main

结果是线程1,2,3随机顺序,Main一定在最后。

去掉join:

        thread1.start();
        thread3.start();
        thread2.start();

情况3输出:

thread Main
thread 3
thread 1
thread 2

这个就是普通的线程执行结果。

我们来分析一下每种情况的原因。join的行为像不像被wait后自动释放的过程?看一下join的实现:

 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) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

可见,线程A在调用obj.join方法时,obj线程如果是alive状态(线程开启start,但未结束),那么就执行wait方法。同时看join(long m)方法是synchronized修饰的,这是我们使用wait时需要先获取锁的前置条件。既然知道了join的内核是wait方法,通过对wait的了解,线程A此时是waiting的状态,并进入了obj锁的等待队列排队去。那么是谁在什么时候释放了线程A呢?

这时要了解一下Thread.exit()方法:

/**
* 这个方法由系统调用,当该线程完全退出前给它一个机会去释放空间。
*/
 private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

这个方法由系统调用,当该线程完全退出前给它一个机会去释放空间。再往下跟到threadTerminated(this)方法

    void threadTerminated(Thread t) {
        synchronized (this) {
            remove(t);

            if (nthreads == 0) {
                notifyAll();
            }
            if (daemon && (nthreads == 0) &&
                (nUnstartedThreads == 0) && (ngroups == 0))
            {
                destroy();
            }
        }
    }

这里有一个notifyAll()的方法,可见就是在这里是线程A得到了释放继续运行。

到这里就解释通了,join的确是通过wait方法使调用线程变为等待状态,再在被调用线程运行结束时通过系统调用exit方法启动了notifyAll。

我们可以这么理解,把obj.join方法替换成obj.wait()方法,并且在obj线程运行结束后自动执行notifyAll()方法,这样就可以用wait的思路来理解join的运行过程了。

好了,现在回到上面例子的三种情况:
情况1: 可以发现,thread2.start前有thread1.join,thread3.start前有thread2.join,这样就能解释thread1,2,3是按顺序执行的。thread1.join的时候,Main线程进入了thread1锁对象的等待队列,只有thread1运行完成后才会得到释放。进而才会按顺序开启线程,thread2.start,thread3.start。

情况2:先将3个线程开启,再依次执行join。join之前,三个线程已经都在运行,所以输出的顺序并没有固定,只是会控制Main线程运行时间。Main的输出肯定是在最后的。当然我们可以把thread2的sleep时间调大一点,并且不执行join再来运行看看结果,自己尝试解释一下。

情况3:没有join,各个线程独自运行,互不影响。

总结

1.wait的注意点: wait方法是Object的方法; wait/notify方法需要获得对象锁后执行。
2.wait方法会把调用线程转为等待waiting状态,释放对象锁,并进入对象锁的等待队列。
3.notify/notifyAll方法会唤醒对象锁的等待队列,使其中的线程进入阻塞blocking状态抢占对象锁。
4.调用notify/notifyAll后,在notify/notifyAll的前获得的对象锁得到释放后,等待队列里的线程才有机会抢占锁继续执行。
5.join方法的内核就是wait。在被调用对象的线程运行完毕后,系统自动调 用被调用对象notifyAll方法。

你可能感兴趣的:(多线程中的wait与join)