Java Object的 wait & notify & notifyAll 方法探索

wait 和 notify/notifyAll 主要是用于线程间通信的方法,即信号量的通信方法。他们是Java Object 的实例方法,所以可以用于各种引用类型。由于wait和notify/notifyAll必须在获取到monitor(锁)的区域内使用,所以,我们更多的是使用synchronized锁住线程共享的变量,并且在共享变量上做线程间通信。

关于wait和notify/notifyAll的使用

举个栗子,关于消费者和生产者模型,会遇到队列(共享变量)为空即没有产品和队列已满的情况;如果是不使用ArrayBlockingQueue等阻塞队列,那么就需要自己对线程间的通信,或者称之为同步。说干就干,写个demo还是不费时间的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Created by Rick on 2017/3/1.
*/
public class UncatchExceptionHandler {
private static List<String> queue = new ArrayList<>(10);
public static void main(String[] args) {
class R implements Runnable {
@Override
public void run() {
for (; ; ) {
synchronized (queue) {
while (queue.size() == 0) {// must == 0
try {
//WAIT here util other thread notify
queue.wait();// same with wait(0)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费数据" + queue.remove(queue.size() - 1) + ",size=" + queue.size());
queue.notify();
//queue.notifyAll();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Thread consumer1 = new Thread(new R());
Thread consumer2 = new Thread(new R());
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
synchronized (queue) {
while (queue.size() == 10) {
try {
queue.wait(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
UUID rand = UUID.randomUUID();
System.out.println("生产数据 :" + rand.toString() + ",size=" + queue.size());
queue.add(rand.toString());
queue.notify();
// queue.notifyAll();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
consumer1.start();
consumer2.start();
producer.start();
}
}

javadoc reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
> The current thread must own this object's monitor.
> This method causes the current thread (call it T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:
> Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened.
> Some other thread invokes the notifyAll method for this object.
> Some other thread interrupts thread T.
> The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.
> The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.
> A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops, like this one:
> synchronized (obj) {
> while ( <condition does="" not="" hold="">)
> obj.wait(timeout);
> ... // Perform action appropriate to condition
> }
> (For more information on this topic, see Section 3.2.3 in Doug Lea's "Concurrent Programming in Java (Second Edition)" (Addison-Wesley, 2000), or Item 50 in Joshua Bloch's "Effective Java Programming Language Guide" (Addison-Wesley, 2001).
> If the current thread is interrupted by any thread before or while it is waiting, then an InterruptedException is thrown. This exception is not thrown until the lock status of this object has been restored as described above.
> Note that the wait method, as it places the current thread into the wait set for this object, unlocks only this object; any other objects on which the current thread may be synchronized remain locked while the thread waits.
> This method should only be called by a thread that is the owner of this object's monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.</condition>

其中

1
2
3
4
5
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}

这句话尤为重要。

wait方法的loop纠结

按照javadoc的解释, wait放在循环体中。but why? 很简单,因为被唤醒意味着有机会执行后续代码,但是,被唤醒的时候也应该检测是否满足条件;
使用while则可以在醒来之后再次检测;如果条件合适(condition hold)那么跳出循环,执行后续代码;如果不合适则再次阻塞。
但是使用if(condition does not hold),则只能在进入时检测,在wait之后就没机会检测了。

notify和notifyAll的区别

notify和notifyAll都可以在改变了共享资源的情况下通知其他等待在该对象的等待队列中的线程,使其从阻塞中醒来。醒来这一步都是相同的,但是之后的就是两者的区别了。
具体来说,notify 将由jvm选取一个等待在该对象的线程使其醒来,并且获得锁,然后继续运行。其他线程既不会醒来,也就没有机会去竞争锁了。
而notifyAll则是将由jvm个等待在该对象的所有线程使其醒来,并且各自竞争锁。所以,如果时间充足并且不发生活锁的情况,那么所有线程将有机会获得锁并且运行下去。
举例来说,比如是这种情况,有线程A,B,C,D,同步对象S,当前BCD阻塞,A线程改变了S对象,并且分别使用notify和notifyAll,那么:
. notify 可能唤醒了C,而且C获取了S的锁,C可以运行下去。跟DB将保持阻塞状态。
. notifyAll 则是唤醒了BCD,这时候BCD同时去竞争S的锁,如果B获取了锁,那么CD将等待锁;当B释放锁之后,CD竞争该锁…

接下来我们看看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Runnable r = new Runnable() {
@Override
public void run() {
synchronized (que) {
while (que.size() == 0) {
try {
que.wait(10000);
System.out.println(Thread.currentThread().getId());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
new Thread(new Runnable() { //Producer
@Override
public void run() {
synchronized (que) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int nextInt = new Random().nextInt(10);
que.add(nextInt + "");
System.out.println("add int " + nextInt + " ...");
que.notify();
System.out.println("Producer Thread " + Thread.currentThread().getId() + " call notifyAll ");
}
}
}).start();

结果是:
notify-> 1
notifyAll -> 2 1 3 4

本文重点:

  1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。
  2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
  3. 永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
  4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
  5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。

code module
code module

如何在 Java 中正确使用 wait, notify 和 notifyAll – 以生产者消费者模型为例
这是关于Java里如何使用wait, notify和notifyAll的所有重点啦。你应该只在你知道自己要做什么的情况下使用这些函数,不然Java里还有很多其它的用来解决同步问题的方 案。例如,如果你想使用生产者消费者模型的话,你也可以使用BlockingQueue,它会帮你处理所有的线程安全问题和流程控制。如果你想要某一个线 程等待另一个线程做出反馈再继续运行,你也可以使用CycliBarrier或者CountDownLatch。如果你只是想保护某一个资源的话,你也可 以使用Semaphore。