所有Java程序都会被编译成类文件,这些文件包含字节码,即Java虚拟机的机器语言。 本文介绍了Java虚拟机如何处理线程同步,包括相关的字节码。本文介绍了与线程同步相关的两个机器码,这两个机器码用于线程同步进入和推出监视器(enter monitor/exit monitor)。
线程和共享数据
Java的优势之一是在语言级别支持多线程。这种支持主要集中在协调对多个线程共享的数据的访问。
JVM将正在运行的Java应用程序的数据组织成几个运行时数据区域:一个或者多个线程栈、一个堆和一个方法区。
在JVM内部,每一个线程被分配一个栈,用于存放此线程单独使用的数据,包括本地变量表、参数和该线程调用的每一个方法的返回值。栈中的数据仅仅包括原始数据类型和对象引用。在JVM中,不可能将实际对象放在线程栈上,所有的对象都在堆上。
在JVM上只有一个堆,所有线程共享此区域。堆上存放了所有的对象。无法在堆上放置单独的原语类型或对象引用–除非这些是其他对象的组成部分。数组也是在堆里的,包括原始类型的数组,但是在Java中,数组本身也是对象。
除了栈和堆,在JVM中存放数据的还有方法区,其包含所有的类(class)变量和静态变量。方法区和栈很相似,也仅仅包括原始数据类型和对象引用。但是类变量是所有线程共享的。
对象级锁和类级锁
通过上面的描述,JVM中有两块内存区域包含了所有线程能共享访问的数据,它们是:
堆,包含了所有的对象
方法区,包含了所有的类变量和静态变量
如果多个线程需要并发的使用同一个对象或者类变量,必须要对他们使用数据做合适的管理。否则,程序将出现不可预测的结果。
为了协调多个线程之间的共享数据访问,JVM将锁与每个对象和类变量关联。 锁就像特权,一次只能有一个线程“拥有”。 如果线程想要锁定特定的对象或类,它将向JVM申请。当线程不再需要锁时,它将锁还给JVM。 如果另一个线程请求了相同的锁,则JVM将锁传递给该线程。
类级别锁的实现实际上和对象锁一样。当JVM加载一个class文件时,它创建了一个java.lang.Class的实例。当你锁定一个类时,你实际上锁定的时那个类的Class对象。
线程无需获取锁即可访问实例或类变量。 但是,如果某个线程确实获得了锁定,则其他线程无法访问锁定的数据,直到拥有该锁定的线程将其释放为止。
监视器
JVM借助于监视器来使用锁。监视器基本上是一个守护者,因为它监视一串代码序列,确保一次只有一个线程执行代码。
每个监视器都与一个对象引用关联。 当线程到达监视器监视下的代码块中的第一条指令时,该线程必须获得对引用对象的锁定。 线程获得锁之前,不允许执行代码。 一旦获得了锁,线程便进入受保护的代码块。当线程离开该块时,无论它如何离开该块,它都会释放关联对象上的锁。
多重锁
允许单个线程多次锁定同一对象。 对于每个对象,JVM都会对该对象被锁定的次数进行计数。 一个未锁定的对象的计数为零。 当线程首次获取锁时,计数将增加为一。 线程每次在同一对象上获取锁时,计数都会增加。 每次线程释放锁定时,计数都会减少。 当计数达到零时,将释放该锁并将其提供给其他线程。
同步块
在Java语言术语中,将访问共享数据的多个线程的协调称为同步。 该语言提供了两种内置的方法来同步对数据的访问:使用同步语句或同步方法。
同步语句块
通过synchronized关键字修饰包含有一个对象引用的表达式来创建一个同步语句,例如下面的代码:
class SyncStatementsDemo{
private List demoLst=new ArraysList(10);
void syncMethod(){
synchronized(this){
if(demoLst!=null&&demoLst.size()>0){
demoLst[1]=demoLst[2];
}
}
}
}
在上述情况下,直到在当前对象(this)上获得锁后,才会执行同步块中包含的语句。 如果表达式产生了对另一个对象的引用,而不是这个this引用,那么在线程继续之前,将获取与该对象相关联的锁。
两个机器码,monitorenter和monitorexit,被用于方法同步块,解释如下:
机器码 描述
monitorenter 弹出对象引用,获取与该对象引用关联的锁
monitorexit 弹出对象引用,释放与该对象引用关联的锁
当JVM遇到monitorenter时,它将获取堆栈上对象引用的对象的锁。 如果线程已经拥有该对象的锁,则将增加一个计数。 每次对对象上的线程执行monitorexit时,计数都会减少。 当计数达到零时,将释放监视器。
请注意,即使同步块内发生异常,catch子句也可以确保锁定的对象将被解锁释放。 无论同步块如何退出,线程进入该块时所获得的对象锁定都将被释放。
同步方法
仅仅需要在方法修饰符上加上synchronized就可以同步整个方法,如下:
class SyncStatementsDemo{
private List demoLst=new ArraysList(10);
synchronized void syncMethod(){
if(demoLst!=null&&demoLst.size()>0){
demoLst[1]=demoLst[2];
}
}
}
JVM不使用任何特殊的操作码来调用同步方法或从同步方法返回。 当JVM解析对方法的符号引用时,它将确定该方法是否时同步的。 如果是,则JVM在调用该方法之前获取一个锁。 对于实例方法,JVM获取与在其上调用该方法的对象关联的锁。 对于类方法,它获取与该方法所属的类关联的锁。 同步方法完成后,无论是通过返回还是方法抛出异常来完成,都将释放该锁。