6.6 使用线程
若想有效使用多线程代码,要对监视器和锁有些基本的认识。你需要知道的要点如下。
同步是为了保护对象的状态和内存,而不是代码。
同步是线程间的协助机制。一个缺陷就可能破坏这种协助模型,导致严重的后果。
获取监视器只能避免其他线程再次获取这个监视器,而不能保护对象。
即便对象的监视器锁定了,不同步的方法也能看到(和修改)不一致的状态。
锁定
Object[]不会锁定其中的单个对象。基本类型的值不可变,因此不能(也无需)锁定。
接口中声明的方法不能使用
synchronized修饰。内部类只是语法糖,因此内部类的锁对外层类无效(反过来亦然)。
Java 的锁可重入(reentrant)。这意味着,如果一个线程拥有一个监视器,这个线程遇到具有同一个监视器的同步代码块时,可以进入这个代码块。2
2除了 Java,其他语言实现的锁并不都有这种特性。
我们还说过,线程可以休眠一段时间。但有时不需要指定具体休眠多久,而是等到满足某个条件时才唤醒。在 Java 中,这种操作通过 wait() 和 notify() 方法完成,这两个方法都在 Object 类中定义。
就像每个 Java 对象都关联一个锁一样,每个对象还会维护一个等待线程列表。在一个线程中,如果某个对象调用了 wait() 方法,那么这个线程会临时释放它拥有的所有锁,而且这个线程会被添加到这个对象的等待线程列表中,然后停止运行。其他线程在这个对象上调用 notifyAll() 方法时,这个对象会唤醒等待线程,让这些线程继续运行。
例如,下面是一个简化版队列,在多线程环境中可以安全使用:
/** 一个线程调用push()方法,把一个对象存入队列。* 另一个线程调用pop()方法,从队列中取出一个对象。* 如果队列中没有数据,pop()方法使用wait()/notify(),一直等待,直到有数据。*/public class WaitingQueue<E> {LinkedList<E> q = new LinkedList<E>(); // 仓库public synchronized void push(E o) {q.add(o); // 把对象添加到链表的末端this.notifyAll(); // 告诉等待的线程,数据准备好了}public synchronized E pop() {while(q.size() == 0) {try { this.wait(); }catch (InterruptedException ignore) {}}return q.remove();}}
这个类在队列为空时(此时 pop() 操作会失败)在 WaitingQueue 实例上调用 wait() 方法。等待的线程会临时释放监视器,允许其他线程声称拥有这个监视器,然后这个线程可能会调用 push() 方法,把新对象添加到队列中。原来的线程被唤醒时,会从它之前开始休眠的地方继续运行,而且会重新获取监视器。
![]()
wait()和notify()方法必须在synchronized修饰的方法或代码块中使用,因为只有临时把锁放弃,这两个方法才能正常工作。
一般来说,大多数开发者都不需要自己编写类似这个示例的类,使用 Java 平台提供的库和组件即可。
