前言
如果查看Java源码java.lang.Object,就能够看到好多地方提到监视器(monitor),都是出现在描述线程竞争关系的时候,比如Object.notify方法和Object.wait方法。
简要描述
监视器是一个用来保证多个线程安全访问共享数据的机制。
监视器和锁的区别?
严格地讲,“监视器”和“锁”这两个术语表达的含义是不一样的,但实际上很多时候两个术语是可以互换的。
- 锁:是一个具有获取和释放语句以维持特定属性的东西,比如:互斥锁,读写锁
- 监视器:是一个机制,或者说是一个概念,用来保证在任何时候只有一个线程能够执行给定区块的代码。“监视器”可以通过“锁”来实现,但“监视器”的含义大于“锁”的含义。
Java中的监视器支持两种线程同步方式:互斥和协作。
- 互斥:在Java虚拟机中,线程互斥是通过对象锁完成的。互斥可以使线程独立使用共享的数据,而不其它线程干扰。
- 协作:在Java虚拟机中,线程协作是通过Object类中的wait和notify方法完成的。协作可以使多个线程为了同一个目标一起工作。
Java中的监视器
可以把监视器理解为一个建筑,这个建筑里有一个特殊的房间,这个房间同一时刻只能被一个线程所占有。这个房间里有一些数据。一个线程从进入该房间到离开该房间,可以全程独占享有该房间的所有数据。进入该建筑叫做进入监视器(entering the monitor),进入该房间叫做获得监视器(acquiring the monitor),独自占有该房间叫做拥有监视器(owning the monitor),离开该房间叫做释放监视器(releasing the monitor),离开该建筑叫做退出监视器(exiting the monitor)。
为了和数据打交道,一个监视器必须关联一些代码。一个监视器区域(一段代码)是一个不可分割的操作,即一个线程在执行监视器区域代码时,不允许其它线程同时执行该监视器区域代码。监视器需要保证同一时间只有一个线程在执行它的监视区域代码。一个线程想要进入该监视器,必须执行到该监视器区域代码的开始处,而该线程如果想要继续前进以执行监视器区域代码,则需要先获得该监视器。
当线程执行到该监视器区域时,会被放进监视器的入口区域(entry set),入口区域(entry set)相当于建筑里面的走廊,这时候相当于进入监视器。
- 如果入口区域中没有其它线程,并且没有其它线程占有监视器,那么该线程就可以获取监视器,并继续执行监视器区域代码。当该线程执行完监视器区域代码,则释放和退出监视器。
- 如果入口区域有其它线程,并且此时有一个线程正占有监视器,则该线程必须在入口区域等待,当占有监视器的线程退出监视器,则新来的线程必须和其它在入口区域中的线程进行竞争,竞争成功的线程才能获取监视器。
互斥方式可以避免线程使用共享数据时被其它线程干扰,而协作方式则帮助多个线程共同完成同一个目标。
当一个线程需要某些状态下的数据时,协作方式就变得很重要。举个例子,如果有两个线程,一个读线程,一个写线程,读线程读出写线程写入缓存的数据。如果缓存区没有数据,则读线程必须等待,等到写线程写入数据之后,读线程才能正常工作。
这种监视器通常被叫做等待和通知监视器(a "Wait and Notify" monitor),在这种类型的监视器中,占有当前监视器的线程可以通过执行等待命令(a wait command)来挂起自己,进入等待区(wait set),进入等待区的线程一直等到该监视器的其它线程执行通知命令(a notify command)才重新被激活。当该监视器里的一个线程执行了通知命令,执行通知命令的线程会继续占有该监视器,直到该线程主动释放该监视器(完成执行监视器区域代码或执行了等待命令进入了等待区)。当发出通知的线程释放了该监视器,在等待区等待的线程才会被复活,重新将会获得监视器。
图中有3个矩形,中间的大矩形仅包含一个线程(占有监视器的线程),左边的小矩形是入口区域(entry set),右边的小矩形是等待区(wait set)。5个标了序号的小门,代表了线程与监视器交互的过程。
整个过程是这样的:
- 当一个线程执行到监视器区域代码开始处时,穿过1号门进入监视器入口区域。
- 如果入口区域中没有其它线程,并且没有其它线程占有监视器,那么该线程就立即穿过2号门以占有监视器,这时候该线程可以继续执行监视器区域代码。如果此时监视器被其它线程占有,该线程则必须在入口区域等待,也因此被阻塞无法继续执行监视器区域代码。
- 如果占有监视器的线程发出等待命令,则穿过3号门释放监视器并进入等待区。
- 如果等待区的线程竞争成功,成功占有监视器,则通过4号门占有监视器,并继续执行监视器区域代码。
- 当占有监视器的线程执行完监视器区域代码,则穿过5号门释放并退出监视器。
注意
- 如果占有监视器的线程未发出过通知命令,则释放并退出监视器后,只有入口区域的线程会竞争获取监视器
- 如果占有监视器的线程发出过通知命令,则释放并退出监视器后,入口区域的线程会同一个或多个等待区的线程共同竞争获取监视器
- Java虚拟机提供了2个通知命令:notify()和notifyAll()。其中notify()方法唤醒任意一个等待区中的线程,notifyAll()方法唤醒等待区中的全部线程。
- Java虚拟机从下一个等待区或入口区选择下一个占有监视器的线程的算法由虚拟机的具体实现决定。