1.对多线程的支持是Java语言的一大优势,Java对线程的控制主要集中在对线程的同步和协作上,Java使用的同步机制是监视器。
监视器
java监视器支持两种线程:互斥和协作。
java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而不干扰地工作。
协作则通过object类的wait方法和notify方法来实现,允许多个线程为了同一个目标而共同工作。
我们将监视器比作一个建筑物,里面有很多房间,房间里面有一些数据,并且同一个时间只能被一个线程占据。一个线程进入房间到离开为止,独占其中全部的数据。
进入这个数据叫做进入监视器。
进入房间叫做获得监视器。
占据房间叫做持有监视器。
离开房间叫做释放监视器。
离开建筑叫做退出监视器。
互斥原理
对一个监视器来说监视区域是最小的,不可分割的代码块。同一个监视器中,监视区域只会被一个线程执行。
当一个线程到达了一个监视区域的开始处,它就会放置到监视器的入口区。如果没有其他线程等待,它就会进入监视器并且获得监视器,当执行完监视区域的代码后,就会释放并退出监视器。
如果当前监视器正在被另外一个线程持有,那么它就会进入一个等待队列(如果有其他线程也在等待进入监视器)。当持有线程退出之后,只能有等待队列中的一个线程进入并持有该监视器,其他的线程仍旧留会在等待队列中。
协作原理
当一个线程需要一些特别的数据,而由另一个线程负责改变这些数据的状态时,同步显得特别重要。
eg,一个读线程会由一个缓冲区中读取数据,而当前这个缓存区是空的,就需要一个写线程写入数据,当写数据完成写入,读线程才能做读取操作。
java虚拟机使用的这种监视器叫做等待并唤醒监视器。
这种监视器中,一个已经持有该监视器的线程,可以通过调用等待命令,暂停自身的执行。当这个线程执行等待命令后,就会释放该监视器并进入一个等待区,并会一直在那里持续等待状态,直到一段时间后该监视器内的其他线程调用了唤醒命令。当一个线程调用唤醒命令后,它会持续持有监视器,直到它主动释放监视器。当执行了唤醒命令或者执行完监视区域,释放监视器后,等待线程会苏醒,其中的一个会重新获得监视器,判断状态条件,以便决定是否继续继续进入等待状态或者退出或者执行监视区域。
Java虚拟机中的这种监视器模型如图20-1所示,将监视器分成了三个区域。中间的大方框包括一个单独的线程,是监视器的持有者;左边小的方框中时入口区;右边另一个小方框是等待区。活动线程用深灰色圆画出,暂停的线程用灰色圆画出。
对象锁
java虚拟机的一些运行时数据区会被所有线程共享,其他的数据是各个线程私有的。
因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问数据进行协调。
1)保存在堆中的实例变量
2)保存在方法区中的类变量
程序不需要协调保存在java栈中的局部变量,因为java栈中的数据是属于拥有该栈的线程私有的。
在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁(有时候被称为互斥提(mutex))。一个锁就像一种任何时候只允许一个线程“拥有”的特权。线程访问实例变量或者类变量不需要获取锁。但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)
类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。
一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减1,当计数器值为0时,锁就被完全释放了。
java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。在java程序中,只需要使用同步语句或者同步方法就可以标志一个监视区域。当每次进入一个监视区域时,java虚拟机都会自动锁上对象或者类。