Java对象头与Monitor
synchronized的锁对象存储在Java对象头里。在jvm的对象头里,主要是由Mark Word以及Class Metadata Address组成。
Class Metadata Address是类型指针指向对象的类元数据。JVM通过这个指针确定该对象在哪个类的实例。
Mark Word存储对象的HashCode、分代年龄、锁标记位等信息。随着对象锁的不同,具体存储的内容并不相同。具体见下表:
图暂略
当synchronized进入到重量级锁的时候,此时指针将会指向monitor对象(监视器锁)的起始地址。
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。数据结构如下:
ObjectMonitor() {
_header = NULL;//markOop对象头
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
结合上述的数据结构以及示意图,我们可以了解到线程状态以及状态转换。
(1)_EntryList:当多个线程访问同一段同步代码,进入_EntryList集合;
(2)线程彼此竞争对象的monitor后进入_Owner区域,owner变量设置为当前线程,同时计数器count+1;
(3)线程如果调用wait(),此时释放当前持有的monitor,owner变量恢复为null.同时该线程进入WaitSet集合中等待被唤醒。
(4)线程执行完毕,则释放monitor并复位变量的值,以便其他线程进入获取monitor.
synchronized代码块
同步语句块的实现是monitorenter指令和monitorexit指令。
线程会去获取对象锁(objectref)所对应的monitor的持有权,此时计数器本身如果为0,则获取到monitor,同时将计数器设置为1。这里如果是同一个线程,那么对于可重入锁来说,计数器会加1进行累计。如果其他线程想要去获取该锁,则此时会被阻塞,直到当前线程被执行完,此时monitorexit指令被执行。
synchronized方法
方法级的同步不同于代码块,对于方法级的同步,JVM在方法表结构中会有个标识,ACC_SYNCHRONIZED访问标志区来分一个方法是否同步方法。
如果被设置了,则执行线程则现持有monitor,然后执行方法,最后方法完成,则释放monitor。
如果遇到异常,则同步方法所持有的monitor将在异常抛到同步方法外自动释放。