Java并发编程(六)——线程的基本操作(下)

179 阅读6分钟

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对象看作多线程间通信的信号量。

wait()和notify()的工作方式如上图所示,obj调用wait()后,当前线程进入obj的等待队列。当其他线程调用obj.notify()后,会从等待队列中随机选择一个线程并唤醒。这里需要注意,这个选择是完全随机的,并不是先进行等待的线程就会优先被唤醒。
此外Object类还有一个notifyAll()方法,它和notify()的用途一样,但是调用后会唤醒等待队列中的所有线程。

使用wait()和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让出给其他线程一些执行机会。