本来是想把标题定为《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) 。
开局一张图:
今天主要就来讲一下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方法。