4. 等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的方法:wait()和notify()。这两个方法并不专属于Thread类,而是属于Object类。也就是说任何对象都可以调用这两个方法。
public final void wait() throws InterruptedException
public final native void notify()
当一个对象调用wait()方法后,当前线程就会在这个对象上等待。比如说线程A中调用了obj.wait()方法,线程A就会挂起进入等待状态,直到其他线程中调用了obj.notify()方法为止。这时,可以将obj对象看作多线程间通信的信号量。
此外Object类还有一个notifyAll()方法,它和notify()的用途一样,但是调用后会唤醒等待队列中的所有线程。
- 线程进入目标对象方法修饰符中包含synchronized关键字的实例方法
- 线程进入对目标对象同步的synchronized代码块
- 当要获取Class类对象的监视器时,线程需要进入到目标类的方法修饰符中包含synchronized关键字的静态方法
当两个线程T1和T2协作时,具体的流程如下所示:
| T1 | T2 |
|---|---|
| 取得object监视器 | |
| object.wait() | |
| 释放object监视器 | |
| 取得object监视器 | |
| object.notify() | |
| 线程被唤醒,开始竞争监视器 | 释放object监视器 |
| 重获object监视器 | |
| 继续执行 |
由于T1调用object.wait()后会释放监视器,所以T2顺利获得监视器并得以执行notify()方法,这里假设唤醒了T1。当T1被唤醒后并不会立刻继续执行后续代码,而是需要重新尝试获取监视器,重获监视器后才可以继续向下执行。为方便理解,使用如下代码作为演示:
public class WNDemo {
final static Object obj = new Object();
public static class T1 extends Thread {
@Override
public void run() {
synchronized(obj) {
try {
System.out.println("T1 wait")
obj.wait();
System.out.println("T1 continue").
} catch (InterruptedException e) {
}
}
System.out.println("T1 end");
}
}
public static class T2 extends Thread {
@Override
public void run() {
// 确保T1优先获得监视器
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized(obj) {
System.out.println("T2 notify");
obj.notify();
System.out.println("T2 end");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
上述代码中t1先获得监视器并进入了synchronized代码块,之后调用了obj.wait()方法,这时t1会进行等待并释放obj的监视器;接着t2进入代码块,执行notify()方法,接着sleep了2000毫秒才退出了synchronized块;这里值得一提的是t2执行notify()两秒之后,t1才能继续执行,这是因为t2休眠时还未退出synchronized块,t1由于t2还未释放对象监视器,所以无法开始竞争。程序执行完毕后我们会获得如下输出:
T1 wait
T2 notify
T2 end
T2 end
Tips: Object.wait()和Thread.sleep()都可以让线程等待若干时间,除了wait()可以手动唤醒外,另一个主要区别就是wait()方法调用后会释放对象锁,而sleep()方法不会释放任何资源。
5. 等待线程结束(join)和谦让(yield)
很多情况下,线程间的协作和人与人之间的协作很类似。一种非常常见的协作方法就是分工合作。举个例子,当开发一款软件时,最开始都是由专门的同事沟通需求,对功能点进行整理和总结,以书面形式给出一份需求文档,然后开发人员一起进行协作开发。也就是说,开发人员需要等待需求分析师完成他的工作,然后才能进行研发。
将这个关系对应到多线程当中,有时一个线程的输入可能依赖于另一个或多个线程的输出,这时这个线程就需要等待其他线程执行完毕,才能继续执行。JDK中通过join()方法来实现这个功能。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一个join()表示无限等待,它会一直阻塞当前线程,知道目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还未执行完毕,当前线程也会因为”等不及“而继续往下执行。这里提供一个简单的join()使用样例,以供大家参考:
public class JoinMain {
public volatile static int i = 0;
public static class JoinThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100000; i++);
}
}
public static void main(String args[]) {
JoinThread t1 = new JoinThread();
t1.start();
t1.join();
System.out.println(i);
}
}
如果在主函数中不使用join(),那么得到的i可能会是0或一个很小的值,因为主线程不会等待t1执行完毕就会将i的值输出。但在使用join()方法后,主线程会等待t1执行完毕再继续向下执行,在join()返回后说明t1已经执行完成,所以此时读取i的值总会是100000。 关于join()的实现,有必要说明一下,下面是JDK中join()实现的核心代码片段。
while(isAlive()) {
wait(0);
}
可以看到,它让调用join()的线程对它进行等待。当被等待的线程执行完毕后在退出前会调用notifyAll()方法通知所有的等待线程继续执行。因此需要注意的一点是:应该尽量避免在应用程序中对Thread类实例进行wait()和notify()等方法,因为这很可能会影响到系统API的工作,或被系统API所影响。
另一个比较有趣的方法是Thread.yield():
public static native void yield();
这是一个静态方法,执行后会使当前线程让出CPU。但让出CPU不代表这个线程就不会执行了,当线程让出CPU后会继续参与对CPU资源的争夺,但是不一定能再次被分配到。因此,对Thread.yield()调用的时机可以是线程执行完一些最重要的任务后,将CPU让出给其他线程一些执行机会。