6.6 使用线程

若想有效使用多线程代码,要对监视器和锁有些基本的认识。你需要知道的要点如下。

  • 同步是为了保护对象的状态和内存,而不是代码。

  • 同步是线程间的协助机制。一个缺陷就可能破坏这种协助模型,导致严重的后果。

  • 获取监视器只能避免其他线程再次获取这个监视器,而不能保护对象。

  • 即便对象的监视器锁定了,不同步的方法也能看到(和修改)不一致的状态。

  • 锁定 Object[] 不会锁定其中的单个对象。

  • 基本类型的值不可变,因此不能(也无需)锁定。

  • 接口中声明的方法不能使用 synchronized 修饰。

  • 内部类只是语法糖,因此内部类的锁对外层类无效(反过来亦然)。

  • Java 的锁可重入(reentrant)。这意味着,如果一个线程拥有一个监视器,这个线程遇到具有同一个监视器的同步代码块时,可以进入这个代码块。2

2除了 Java,其他语言实现的锁并不都有这种特性。

我们还说过,线程可以休眠一段时间。但有时不需要指定具体休眠多久,而是等到满足某个条件时才唤醒。在 Java 中,这种操作通过 wait()notify() 方法完成,这两个方法都在 Object 类中定义。

就像每个 Java 对象都关联一个锁一样,每个对象还会维护一个等待线程列表。在一个线程中,如果某个对象调用了 wait() 方法,那么这个线程会临时释放它拥有的所有锁,而且这个线程会被添加到这个对象的等待线程列表中,然后停止运行。其他线程在这个对象上调用 notifyAll() 方法时,这个对象会唤醒等待线程,让这些线程继续运行。

例如,下面是一个简化版队列,在多线程环境中可以安全使用:

  1. /*
  2. * 一个线程调用push()方法,把一个对象存入队列。
  3. * 另一个线程调用pop()方法,从队列中取出一个对象。
  4. * 如果队列中没有数据,pop()方法使用wait()/notify(),一直等待,直到有数据。
  5. */
  6. public class WaitingQueue<E> {
  7. LinkedList<E> q = new LinkedList<E>(); // 仓库
  8. public synchronized void push(E o) {
  9. q.add(o); // 把对象添加到链表的末端
  10. this.notifyAll(); // 告诉等待的线程,数据准备好了
  11. }
  12. public synchronized E pop() {
  13. while(q.size() == 0) {
  14. try { this.wait(); }
  15. catch (InterruptedException ignore) {}
  16. }
  17. return q.remove();
  18. }
  19. }

这个类在队列为空时(此时 pop() 操作会失败)在 WaitingQueue 实例上调用 wait() 方法。等待的线程会临时释放监视器,允许其他线程声称拥有这个监视器,然后这个线程可能会调用 push() 方法,把新对象添加到队列中。原来的线程被唤醒时,会从它之前开始休眠的地方继续运行,而且会重新获取监视器。

6.6 使用线程 - 图1 wait()notify() 方法必须在 synchronized 修饰的方法或代码块中使用,因为只有临时把锁放弃,这两个方法才能正常工作。

一般来说,大多数开发者都不需要自己编写类似这个示例的类,使用 Java 平台提供的库和组件即可。