等待和唤醒
Q:wait()
,notify()
和 notifyAll()
是用来干什么的?
A:wait()
,notify()
和 notifyAll()
可以让线程协调完成一项任务。例如,一个线程生产,另一线程消费。生产线程不能在前一产品被消费之前运行,而应该等待前一个被生产出来的产品被消费之后才被唤醒,进行生产。同理,消费线程也不能在生产线程之前运行,即不能消费不存在的产品,所以应该等待生产线程执行一个之后才执行。利用这些方法,就可以实现这些线程之间的协调。从本质上说,一个线程等待某种状态(例如一个产品被生产),另一个线程正在执行,知道产生了某种状态(例如生产了一个产品)。
Q:不同的 wait()
方法之间有什么区别?
A:没有参数的 wait()
方法被调用之后,线程就会一直处于睡眠状态,直到本对象(就是 wait()
被调用的那个对象)调用 notify()
或 notifyAll()
方法。相应的,wait(long timeout)
和wait(long timeout, int nanos)
方法中,当等待时间结束或者被唤醒(无论哪一个先发生)时将会结束等待。
Q:notify()
和 notifyAll()
方法有什么区别?
A:notify()
方法随机唤醒一个等待的线程,而 notifyAll()
方法将唤醒所有在等待的的线程。
Q:线程被唤醒之后会发生什么?
A:当一个线程被唤醒之后,不会立即执行,除非本对象(调用 notify()
或 notifyAll()
的对象)的同步锁被释放。唤醒的线程会按照规则和其他线程竞争同步锁,得到锁的线程将执行。所以 notifyAll()
方法执行之后,可能会有一个线程立即运行,也可能所有的线程都没运行。
Q:为什么在使用等待、唤醒方法时,要放在同步代码中?
A::将等待和唤醒方法放在同步代码中是非常必要的,这样做是为了避免出现竞争条件。鉴于要等待的线程通常在调用
wait()之前会确认一种情况存在与否(通常是检查某一变量的值),而另一线程在调用
notify()` 之前通常会设置某种情况(通常是通过设置一个变量的值)。以下的这种情况就是发生了竞争条件:
- 线程一检查了情况和变量,发现需要等待。
- 线程二设置了变量。
- 线程二调用了
notify()
。此时,线程一还没有等待,所以这次调用什么用都没有。 - 线程一调用了
wait()
。这下它永远不会被唤醒了。
Q:如果在同步代码之外使用这些方法会怎么样呢?
A:如果在同步代码之外使用了这些情况,就会跑出java.lang.IllegalMonitorStateException
异常。
Q:如果在同步代码中调用这些方法呢?
A:当 wait()
方法在同步代码中被调用时,会根据同步代码中方法的优先级先后执行,在 wait()
方法返回值之前,该同步代码一直持有锁,这样就不会出现竞争条件了。在 wait()
方法可以接受唤醒之前,锁一直都不会被释放。
Q:为什么要要把 wait()
的调用放在 while
循环中,而不是 if
判断中呢?
A:为了防止假唤醒,可以在 stackoverflow 上了解有关这类现象的更多信息——假唤醒真的会发生吗?。
Q:能提供一个使用等待与唤醒方法的范例吗?
A:见代码清单2.
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 81 82 83 84 85 86 87 88 89 |
public class WaitNotifyDemo { public static void main(String[] args) { class Shared { private String msg; synchronized void send(String msg) { while (this.msg != null) try { wait(); } catch (InterruptedException ie) { } this.msg = msg; notify(); } synchronized String receive() { while (msg == null) try { wait(); } catch (InterruptedException ie) { } String temp = msg; msg = null; notify(); return temp; } } final Shared shared = new Shared(); Runnable rsender; rsender = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { shared.send("A"+i); try { Thread.sleep((int)(Math.random()*200)); } catch (InterruptedException ie) { } } shared.send("done"); } }; Thread sender = new Thread(rsender); Runnable rreceiver; rreceiver = new Runnable() { @Override public void run() { String msg; while (!(msg = shared.receive()).equals("done")) { System.out.println(msg); try { Thread.sleep((int)(Math.random()*200)); } catch (InterruptedException ie) { } } } }; Thread receiver = new Thread(rreceiver); sender.start(); receiver.start(); } } |
代码清单2:发送与接收信息
代码清单2声明了一个 WaitNotifyDemo
类,其中,main()
方法有一对发送和接收信息的线程。
main()
方法首先声明了 Shard
本地类,包含接收和发送信息的任务。Share
声明了一个 String
类型的smg
私有成员变量来存储要发送的信息,同时声明了同步的 send()
和 receive()
方法来执行接收和发送动作。
发送线程调用的是 send()
。因为上一次调用 send()
的信息可能还没有被接收到,所以这个方法首先要通过计算 this.msg != null
的值来判断信息发送的状态。如果返回值为真,那么信息处于被等待发送的状态,就会调用 wait()
,一旦信息被接收到,接受的线程就会给 msg
赋值为 null
,并存储新信息,调用notify()
唤醒等待的线程。
接收线程调用的是 receive()
因为可能没有信息处于被接收状态,这个方法首先会通过计算 mas == null
的值来验证信息有没有等待被接收的状态。如果表达式返回值为真,就表示没有信息等待被接收,此线程就要调用 wait()
方法。如果有信息发送,发送线程就会给 msg
分配值并且调用 notify()
唤醒接收线程。
编译(javac WaitNotifyDemo.java
)并运行(java WaitNotifyDemo
)源代码,将会看到以下输出结果:
1 2 3 4 5 6 7 8 9 10 |
A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 |
Q:我想更加深入的学习等待和唤醒的机制,能提供一些资源吗?
A:可以在 artima 参考 Bill Venners 的书《Inside the Java Virtual Machine(深入理解 java 虚拟机)》中第20章 Chapter 20: Thread Synchronization.
Object,接口和 java8
Q:在第一部分中提到过接口是不继承 Object
的。然而,我发现有些接口中声明了 Object
中的方法。比如,java.util.Comparator
接口有 boolean.equals(Object.obj)
.为什么呢?
A:Java语言规范的 9.6.3.4 部分中清楚说明了,接口有相当于 Object
中成员那样的公共抽象成员。此外,如果接口中声明了 Object
中的成员函数(例如,声明的函数相当于覆盖 Object
中的 public
的方法),则认为是接口覆盖了他们,可以用 @Orride
注释。
为什么要在接口中声明 public
的非final
object
方法(可能还带有 @Override
)呢?举例来说,Comparator
接口中就有 boolean equals(Object obj)
声明,这个方法在接口中声明是为了此接口的特殊情况。
此外,这个方法只有在传入的类是一个比较规则相同的比较器的时候,才能返回
true
。
因为这种情况是可选的,所以并不强制实现 Comparator
,这取决于有没有 equals
只有在遇到一个比较规则相同的比较器的时候才返回真的需求。尽管类并不要求覆盖 equals
,但是文档中却支持这样做来提高性能。
注意,不覆盖
Object.equals(Object)
是安全的,但是覆盖这个方发可能在一些情况下提高性能,比如让程序判断两个不同的比较器是不是用的相同的规则。
Q:哪一个 Employee
方法被覆盖了?是 Object
中的,还是接口中的?
A:更早的文档中说,被覆盖的方法是 Object
中的。
Q:java 8支持接口中的默认方法。可以在接口中默认实现 Employee
方法或者 Object
中的其他方法吗?
A:不可以。Object
中的任何 public
的非 final
方法都是不允许在接口中默认实现的。这个限制的基本原理在 Brian Goetz 的 允许默认方法覆盖Object
中的方法 一文中有说明。
Q:能提供更多关于接口中 Object
方法的学习资源吗?
A:可以参考这篇 接口继承了 Object
类吗?。
—-
至此,Jeff Friesen所有有关 Object 的介绍就结束了。查看目录请看第一部分。