Synchronized的底层实现原理
如果我们要想了解synchronized底层实现原理,那么不妨先来了解了解对象在堆中的存储结构
java对象在堆中的存储结构分为三个部分,分别为对象头、实例变量和填充字节。
- 对象头里面主要包含MarkWord和Klass Point类型指针,MarkWord里面存储的是对象自身的运行时数据(包含了Hashcode、GC分带年龄、锁状态标记、线程持有的锁等等),而Klass Point则指向的是类元数据的指针,JVM就是通过此指针来确定这个对象是哪个类的实例。
- 实例变量里面存储的是对象的属性信息,包括父类的属性信息,按照4个字节对齐
- 填充字节则是因为java虚拟机要求对象字节数必须是8的整数倍,填充字节就是用于凑齐这个倍数用的。
在JVM中,synchronized的对象锁,其指针指向的是一个monitor对象(由C++实现的ObjectMonitor对象)的起始地址。它的大致数据结构如下:ObjectMonitor() { _count = 0; //用来记录该对象被线程获取锁的次数 _waiters = 0; _recursions = 0; //锁的重入次数 _owner = NULL; //指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 }
我们知道线程的生命周期分为5个状态,分别为start,runing,waiting、blocking、dead
当synchronized修饰方法和代码块时,流程如下:
- 当多个线程同时访问该方法时候,这些线程会先进入_EntryList队列,此时这些线程处于blocking状态
- 当某一个线程获取到了实例对象的监视器(monitor)锁,那么就进入running状态,执行线程的方法,此时ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象被一个线程获取
- 当running状态的线程调用wait()方法的时候,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner置为null,_count减1,同时该线程进入_WaitSet队列,直到有线程调用notify或者notifyAll方法唤醒该线程,则该线程重新获取到monitor对象进入running状态
- 如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner置为null,_count减1
那么synchronized又是如何获得monitor对象呢? - synchronized如果修饰的是代码块,则是在代码块开始的位置插入monitorenter命令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorenter和monitorexit成对出现,故monitorexit出现两次,防止异常导致无法配对,我们查看字节码会发现其代码如下:
- synchronized如果修饰的是方法,则不是通过插入monitorenter和monitorexit指令实现的,而是通过设置方法表结构中的ACC_SYNCHRONIZED标识,方法在执行时,会读取这个标识,如果发现有此标识则会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕则释放monitor对象,所谓的获取monitor对象和释放monitor对象指的就是操作monitor对象的_owner和_count属性,我们查看字节码会发现其代码如下: