Java多线程之线程协作

Java多线程之线程协作

一、前言

  上一节提到,如果有一个线程正在运行synchronized 方法,那么其他线程就无法再运行这个方法了。这就是简单的互斥处理。

  假如我们现在想执行更加精确的控制,而不是单纯地等待其他线程运行终止,例如下面这样的控制。

  ● 如果空间为空则写入数据;如果非空则一直等待到变空为止

  ● 空间已为空时,“通知”正在等待的线程

  此处是根据“空间是否为空”这个条件来执行线程控制的。Java 提供了用于执行线程控制的wait 方法、notify 方法和notifyAll 方法。wait 是让线程等待的方法,而notify 和notifyAll 是唤醒等待中的线程的方法。

二、等待队列——线程休息室

  在学习wait、notify 和notifyAll 之前,我们先来学习一下等待队列。所有实例都拥有一个等待队列,它是在实例的wait方法执行后停止操作的线程的队列。打个比方来说,就是为每个实例准备的线程休息室。

  在执行wait 方法后,线程便会暂停操作,进入等待队列这个休息室。除非发生下列某一情况,否则线程会一直在等待队列中休眠。当下列任意一种情况发生时,线程便会退出等待队列。

  ● 有其他线程的notify方法来唤醒线程

  ● 有其他线程的notifyAll方法来唤醒线程

  ● 有其他线程的interrupt方法来唤醒线程

  ● wait方法超时

  下面以图配文依次谈谈wait、notify 和notifyAll。而关于interrupt 方法和wait 方法的超时,将会在后面的篇幅中谈谈。

三、wait 方法——将线程放入等待队列

  wait(等待)方法会让线程进入等待队列。假设我们执行了下面这条语句。

  obj.wait();

  那么,当前线程便会暂停运行,并进入实例obj的等待队列中。这叫作“线程正在obj 上wait”。如果实例方法中有如下语句(1),由于其含义等同于(2),所以执行了wait() 的线程将会进入this 的等待队列中,这时可以说“线程正在this 上wait”。

  wait();  (1)

  this.wait(); (2)

  若要执行wait方法,线程必须持有锁(这是规则)。但如果线程进入等待队列,便会释放其实例的锁。整个操作过程如下图所示。

  • 关于等待队列

  等待队列是一个虚拟的概念。它既不是实例中的字段,也不是用于获取正在实例上等待的线程的列表的方法。

  • 获取锁了的线程A执行wait方法:

Java多线程之线程协作

  • 线程A进入等待队列,释放锁:

Java多线程之线程协作

  • 线程B能够获取锁:

Java多线程之线程协作

四、notify 方法——从等待队列中取出线程

  notify(通知)方法会将等待队列中的一个线程取出。假设我们执行了下面这条语句。

  obj.notify();

  那么obj 的等待队列中的一个线程便会被选中和唤醒,然后就会退出等待队列。

  整个操作过程如下所示。

  • 获取锁了的线程B执行notify方法:

Java多线程之线程协作

  • 线程A退出等待队列,想要进入wait的下一个操作,但刚才执行notify方法的线程B任持有着锁

Java多线程之线程协作

  • 刚才执行notify的线程B释放了锁

Java多线程之线程协作

  • 退出等待队列的线程A获取锁,执行wait的下一步操作

Java多线程之线程协作

  同wait 方法一样,若要执行notify 方法,线程也必须持有要调用的实例的锁(这是规则)。

  执行notify 后的线程状态:

  notify 唤醒的线程并不会在执行notify 的一瞬间重新运行。因为在执行notify 的那一瞬间,执行notify 的线程还持有着锁,所以其他线程还无法获取这个实例的锁(如第二幅图所示)。

  执行notify 后如何选择线程?

  假如在执行notify 方法时,正在等待队列中等待的线程不止一个,对于“这时该如何来选择线程”这个问题规范中并没有作出规定。究竟是选择最先wait 的线程,还是随机选择,或者采用其他方法要取决于Java 平台运行环境。因此编写程序时需要注意,最好不要编写依赖于所选线程的程序。

五、notifyAll 方法——从等待队列中取出所有线程

  notifyAll(通知大家)方法会将等待队列中的所有线程都取出来。例如,执行下面这条语句之后,在obj 实例的等待队列中休眠的所有线程都会被唤醒。

  obj.notifyAll();

  如果简单地在实例方法中写成下面(1)这样,那么由于其含义等同于(2),所以该语句所在方法的实例(this)的等待队列中所有线程都会退出等待队列。

  notifyAll();  (1)

  this.notifyAll(); (2)

  下面两幅图展示了notify 方法和notifyAll 方法的差异。notify 方法仅唤醒一个线程,而notifyAll 则唤醒所有线程,这是两者之间唯一的区别。

  • notify方法仅唤醒一个线程,并让该线程退出等待对列:

Java多线程之线程协作

  • notifyAll方法唤醒所有线程,并让所有线程都退出等待队列

Java多线程之线程协作

  同wait 方法和notify 方法一样,notifyAll 方法也只能由持有要调用的实例的锁的线程调用。

  刚被唤醒的线程会去获取其他线程在进入wait 状态时释放的锁。但现在锁是在谁的手中呢?对,就是执行notifyAll 的线程正持有着锁。因此,唤醒的线程虽然都退出了等待队列,但都在等待获取锁,处于阻塞状态。只有在执行notifyAll 的线程释放锁以后,其中一个幸运儿才能够实际运行。

  那如果线程未持有锁会怎样呢?

  如果未持有锁的线程调用wait、notify 或notifyAll, 异常java.lang.IllegalMonitorStateException会被抛出。

  该使用notify 方法还是notifyAll 方法呢?

  notify 方法和notifyAll 方法非常相似,到底该使用哪一个呢?实际上,这很难选择。

  由于notify 唤醒的线程较少,所以处理速度要比使用notifyAll 时快。

  但使用notify 时,如果处理不好,程序便可能会停止。一般来说,使用notifyAll 时的代码要比使用notify 时的更为健壮。

  除非开发人员完全理解代码的含义和范围,否则使用notifyAll 更为稳妥。使用notify时发生问题的示例将在后面探讨,详情可以关注我的博文。

六、wait、notify、notifyAll 是Object 类的方法

  wait、notify 和notifyAll 都是java.lang.Object 类的方法,而不是Thread 类中固有的方法。

  下面再来回顾一下wait、notify 和notifyAll 的操作。

  ● obj.wait()是将当前线程放入obj的等待队列中

  ● obj.notify()会从obj的等待队列中唤醒一个线程

  ● obj.notifyAll()会从obj的等待队列中唤醒所有线程

  换句话说, wait、notify 和notifyAll 这三个方法与其说是针对线程的操作,倒不如说是针对实例的等待队列的操作。由于所有实例都有等待队列,所以wait、notify 和notifyAll也就成为了Object 类的方法。

  wait、notify、notifyAll 也是Thread 类的方法:

  wait、notify 和notifyAll 确实不是Thread 类中固有的方法。但由于Object 类是Java 中所有类的父类,所以也可以说wait、notify 和notifyAll 都是Thread 类的方法。关于wait、notify 和notifyAll 的用法,后面的篇幅中将会详细解说。

  参考:图解Java多线程设计模式

上一篇:java 多线程二


下一篇:Java多线程基础知识笔记(持续更新)