区别的核心是,上面描述的所有方法在阻塞时都不会释放被占用的锁,但是这个相反的规则是相反的。上述核心差异导致了一系列细节上的差异。首先,上面提到的所有方法都属于Thread类,但是这一对直接属于Object类,也就是所有对象都有这一对方法。乍一看,这是不可思议的,但实际上这是非常自然的,因为当这一对方法阻塞时,它们必须释放被占用的锁,这个锁是任何对象所拥有的。调用任何对象的等待方法都会导致线程阻塞,并且该对象上的锁被释放。调用任何对象的notify方法都会导致取消阻塞从通过调用该对象的wait方法阻塞的线程中随机选择的线程。
其次,上面描述的所有方法都可以在任何地方调用,但是这一对方法必须在同步的方法或块中调用。原因也很简单。只有在同步的方法或块中,当前线程才能占用锁,锁才能被释放。同样,调用这一对方法的对象上的锁必须由当前线程拥有,这样锁才能被释放。因此,这一对方法调用必须放在同步的方法或块中,其锁定的对象是调用这一对方法的对象。如果不满足这个条件,程序还是可以编译的,但是运行时会出现IllegalMonitorStateException。
等待和通知方法的上述特征决定了它们通常与synchronized关键字一起使用。通过与操作系统的进程间通信机进行对比,可以发现它们的相似之处:同步的方法或块提供了类似于操作系统原语的功能,其执行不会受到多线程机制的干扰,相当于块和唤醒原语。它们的结合使我们能够在操作系统上实现一系列微妙的进程间通信算法,可以用来解决各种复杂的线程间通信问题。
关于等待和通知方法,还有最后两点:
首先,通过调用notify方法解除阻塞的线程是从通过调用对象的wait方法阻塞的线程中随机选择的。我们无法预测会选择哪个线程,所以在编程时要特别小心,避免这种不确定性带来的问题。
第二,除了notifyAll,还有一个方法notifyAll可以起到类似的作用。唯一的区别是,调用notifyAll方法将取消阻止所有通过一次调用此对象的等待方法而被阻止的线程。当然,只有获得锁的线程才能进入可执行状态。
说到阻塞,就不能不说死锁了。经过简要分析,我们可以发现,在没有指定超时期限的情况下调用挂起方法和等待方法可能会导致死锁。遗憾的是,Java在语言层面不支持死锁避免,所以在编程时一定要小心避免死锁。
以上,我们分析了Java中线程阻塞的各种方法。我们把重点放在等待和通知方法上,因为它们是最强大和最灵活的使用方法,但这也导致了它们的低效率和容易出错。在实践中,我们应该灵活运用各种方法,以便更好地实现我们的目标。