深入虚拟机笔记之线程同步

第20章 线程同步

    监视器:

    java所使用的同步机制是监视器,java中的监视器支持两种线程:互斥和协作。java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而互不干扰地工作。协作则是通过Object类的wait、notify和notifyAll方法来实现,允许多个线程为了同一个目标而共同工作。

    除了与数据关联之外,监视器还会关联到一些或更多的代码,这样的代码被称作监视区域。对于一个监视器来说,监视区域是最小的、不可分割的代码块。换句话说,在同一个监视器中,监视区域只会同时被一个线程执行,即使同时有多个并发的线程,监视器会保证在监视区域上同一时间只会执行一个线程。一个线程想要进入监视器的唯一途径就是到达监视器所关联的一个监视区域的开始处,而线程想要继续执行监视区域的唯一途径就是获得该监视器。

    当一个线程到达一个监视区域的开始出,它就会被放置到该监视器的入口区。如果没有其他线程在入口区总等待,也没有线程正持有监视器,则这个线程就可以获得监视,并继续执行监视区域中的代码;当这个线程执行完监视区域后,它就会退出并释放该监视器。如果已经有线程持有该监视器,则这个刚刚到达的线程必须在入口区等待,当监视器的持有者退出监视器后,新到达的线程必须与其他已经在入口区等待的线程进行一次比赛,最终只会有一个线程赢得比赛并获得监视器。

    互斥帮助线程在访问共享数据时不被其他线程干扰,而协作帮助线程与其他线程共同工作。java虚拟机所使用的这种监视器被称作“等待并唤醒”监视器(也被称作“发信号并继续”)。在这种监视器中,一个已经持有监视器的线程,可以通过一个等待命令,暂停自身的执行;当线程执行了等待命令后,它会释放监视器,并进入一个等待区,这个线程会在等待区一直持续暂停状态,直到这个线程中的其他线程执行了唤醒命令。当

一个线程执行了唤醒命令后,它会继续持有监视器,直到它主动释放监视器(执行一个等待命令或者执行完监视区域的代码)。当执行唤醒的线程释放了监视器后,等待线程才会苏醒,并重新获得监视器。

    唤醒线程在它将监视器保护数据值为等待线程想要的状态后执行唤醒命令,但是因为唤醒线程会继续执行,它可能会在执行唤醒后又修改了数据的状态,让等待线程不能继续工作。另一种情况是,第三个线程可能在唤醒线程释放了监视器,而等待线程还没有获得监视器之前抢先获得监视器,而这个线程可能会修改监视器保护的数据的状态。因为以上事实,一次唤醒往往被等待线程看作是一次提醒,告诉它“数据已经是你想要的状态了”。每次等待线程苏醒并获得监视器的时候,它都需要再次检查数据的状态,以确定是否可以继续完成工作;如果数据不是他所需要的状态,这个线程可能会再次执行等待命令或者放弃等待退出监视器。

    活动线程会通过两条途径释放监视器:执行一个等待命令,或者完成它正在执行的监视区域。

    如果一个监视器的持有者在它释放监视器前没有执行唤醒命令(同时在此之前也没有任何等待线程被唤醒并等待苏醒),那么位于入口区的线程将竞争获得监视器。如果当前持有者执行了唤醒命令,那么入口区中的线程就不得不与一个或多个等待区中的线程竞争。一个线程只有在它正持有监视器时才能执行等待命令,而且它只能通过再次成为监视器的持有者才能离开等待区。

    在java虚拟机中,线程在执行等待命令时可以随意指定一个暂停时间,如果在暂停时间截止之前没有其他线程执行唤醒命令,那么这个等待线程会从虚拟机中得到一个自动唤醒的命令,也就是说,在暂停时间到了之后,即使没有来自其他线程的明确的唤醒命令,它也会自动苏醒。

    java虚拟机提供了两种唤醒命令:notify和notifyAll。notify命令随机从等待区中选择一个线程并将其标志为可能苏醒,而notifyAll命令会将等待区中的所有线程都标志为可能苏醒。

    java虚拟机如何从等待区以及入口区选择下一个线程来执行,在很大程度上取决于java虚拟机的设计者。程序员必须不依赖任何特定的有关优先级的算法或安排。只有当绝对确认只会有一个线程在等待区中挂起的时候,才应该使用notify;只要存在同时有多个线程在等待区中被挂起的可能性,就应该使用notifyAll。

    对象锁:

    java虚拟机的一些运行时数据区会被所有的线程共享,其他的数据是各个线程私有的。因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问的数据进行协调:保存在堆中的实例变量、保存在方法区中的类变量。程序不需要协调保存在java栈中的局部变量,java栈中的数据是属于线程私有的。

    在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象,监视器保护对象的实例变量;对于类,监视器保护类的类变量。如果一个对象没有实例变量,或者一个类没有类变量,相关联的监视器就什么都不监视。

    为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁(互斥体mutex)。线程访问实例变量或者类变量不需要获取锁;但是如果线程获取了对象或类的锁,那么在它释放这个锁之前,就没有其他线程可以获取同一个对象或类的锁(锁住一个对象就是获取相关联的监视器)。

    类的锁实际上用对象锁实现,当java虚拟机装载一个class文件的时候,它会创建一个java.lang.Class类的实例来代表该类型;当锁住一个类的时候,实际上锁住的是那个类的Class对象。

    一个线程可以允许多次对同一个对象上锁。对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁。没有被锁的对象的计数器是0,线程每加锁一次,计数器加1(只有已经拥有这个对象的锁的线程才能对该对象再次加锁,在它释放锁之前,其他线程不能对这个对象加锁);线程每释放一次锁,计数器就减1。当计数器为0的时候,锁就被完全释放了,其他的线程才可以使用它。

    java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁。java中有两种监视区域:同步块和同步方法。当线程到达监视区域的第一条指令的时候,线程必须对该引用对象加锁,否则线程不允许执行其中的代码。一旦它获得了锁,线程就进入被保护的代码;当线程离开这块代码时,不管它是如何离开的,它都会释放相关对象上的锁。

    java程序员不需要自己动手加锁,对象锁是在java虚拟机内部使用的。在java程序中,只需要编写同步块或者同步方法就可以标志一个监视区域。当java虚拟机运行程序的时候,每一次进入一个监视区域的时候,它都会自动锁上对象或者类的Class对象。

    同步语句块:方法内的同步语句块会使用monitorenter和monitorexit操作码。当虚拟机遇到monitorenter的时候,它获得栈中objectref所引用对象的锁,如果线程已经拥有那个对象的锁,锁的计数器加1。线程中的每条monitorexit都会引起计数器减1。class文件的字节码中总会使用catch子句来确保被加锁的对象将被释放,即使从同步语句块中抛出异常。不管被同步的语句块是如何退出的,线程进入这个块时获得的锁总是一定会被释放的。

    同步方法:java虚拟机调用同步方法或者从同步方法中返回没有使用任何特别的操作码。当虚拟机解析对方法的符号引用时,它判断这个方法是否是同步的;如果是同步的,虚拟机就在调用方法之前获取一个锁。当同步方法执行完毕的时候,不管是正常结束还是抛出异常,虚拟机都会释放这个锁。

    同步方法的类在编译为字节码时,没有使用进入和离开监视的指令,也没有为方法创建同步块的异常表,所以更加高效。

    Object类中的协调支持:Object类声明了5个方法,用来访问java虚拟机同步的协调支持。这个方法都是被声明成public final的,所以它们被所有类继承。只有在同步语句块或者同步方法中才能调用这些方法。换句话说,在这些方法被调用的时候,相关联的对象必须已经被加锁了。

 

猜你喜欢

转载自jaesonchen.iteye.com/blog/2290035