前言
前面介绍了synchronized的用法和基本实现原理,synchronized是通过JVM内部的监视器锁(monitor)来实现的,但是监视器锁本质又是依赖于底层操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么说synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量级锁,JDK1.6之后对synchronized做的种种优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,下面将从JVM源码层面查看这些优化的实现原理。
对象头和内置锁(ObjectMonitor)
根据jvm的分区,对象分配在堆内存中,其在64位机器的内存布局如下:
对象头
Hotspot虚拟机的对象头包括两部分,第一部分用于储存对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、锁指针等,这部分数据在32bit和64bit的虚拟机中大小分别为32bit和64bit,官方称它为**Mark word。考虑到虚拟机的空间效率,Mark word**被设计成一个非固定的数据结构以便在极小的空间中存储尽量多的信息,它会根据对象的状态复用自己的存储空间,详细情况如下图:
对象头的另外一部分是类型指针(klass pointer),即对象指向它的类元数据的指针,在64bit的虚拟机中如果开启指针压缩占用大小为32bit,否则为64bit。如果对象访问定位方式是句柄访问,那么该部分没有,如果是直接访问,该部分保留。
句柄访问方式如下图:
直接访问如下图:
内置锁(ObjectMonitor)
我们通常所说的对象的内置锁,是对象头**Mark word**中的重量级锁指针指向的monitor对象,该对象是在HotSpot底层使用C++语言编写的,C++源码中ObjectMonitor结构如下:
1 | //结构体如下 |
ObjectMonitor队列之间的关系转换可以用下图表示:
既然提到了_waitSet和_EntryList(_cxq队列后面会说),那就看一下底层的wait()和notify()方法。
wait()方法的实现过程:
1 | //1.调用ObjectSynchronizer::wait方法 |
总结:通过object获得内置锁(objectMonitor),通过内置锁将Thread封装成OjectWaiter对象,然后addWaiter()将它插入以_waitSet为首结点的等待线程链表中去,最后释放锁。
notify()方法的底层实现
1 | //1.调用ObjectSynchronizer::notify方法 |
总结:通过object获得内置锁(objectMonitor),调用内置锁的notify()方法,通过_waitset结点移出等待链表中的首结点,将它置于_EntrySet中去,等待获取锁。
注意:notifyAll()根据policy不同可能移入_EntryList或者_cxq队列中,此处不详谈。

synchronized的底层原理
此处我们只讨论重量级锁(ObjectMonitor)的获取情况,其他锁的获取放在后面synchronzied的优化中进行说明。
monitorenter源码如下:
1 | void ATTR ObjectMonitor::enter(TRAPS) { |
总结:
- 如果
monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的owner - 如果线程已经占有该
monitor,只是重新进入,则进入monitor的进入数加1 - 如果其他线程已经占用了
monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
synchronized的优化
在了解了synchronized重量级锁效率特别低之后,jdk1.6之后做了一些优化,出现了偏向锁,轻量级锁,重量级锁,自旋等优化,我们应该改正monitorenter指令就是获取对象重量级锁的错误认识,很显然,优化之后,锁的获取判断次序是无锁->偏向锁->轻量级锁->重量级锁。
偏向锁
源码如下:
1 | //偏向锁入口 |
BiasedLocking::revoke_and_rebias调用过程如下流程图:
偏向锁的撤销过程如下:
轻量级锁
轻量级锁获取源码:
1 | //轻量级锁入口 |
轻量级锁获取流程如下:

轻量级锁撤销源码:
1 | void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) { |
轻量级锁撤销流程如下:
轻量级锁膨胀
源代码:
1 | ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { |
轻量级锁膨胀流程图:
- 为什么在撤销轻量级锁的时候会有失败的可能?
假设thread1拥有了轻量级锁,Mark Word指向thread1栈帧,thread2请求锁的时候,就会膨胀初始化ObjectMonitor对象,将Mark Word更新为指向ObjectMonitor的指针,那么在thread1退出的时候,CAS操作会失败,因为Mark Word不再指向thread1的栈帧,这个时候thread1自旋等待infalte完毕,执行重量级锁的退出操作
重量级锁
重量级锁的获取入口:
1 | void ATTR ObjectMonitor::enter(TRAPS) { |
进入EnterI (TRAPS)方法(这段代码个人觉得很有意思):
1 | void ATTR ObjectMonitor::EnterI (TRAPS) { |
try了那么多次lock,接下来看下TryLock:
1 | int ObjectMonitor::TryLock (Thread * Self) { |
重量级锁获取入口流程图:
重量级锁的出口:
1 | void ATTR ObjectMonitor::exit(TRAPS) { |
ExitEpilog用来唤醒线程,代码如下:
1 | void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { |
重量级锁出口流程图:
自旋
通过对源码的分析,发现多处存在自旋和tryLock操作,那么这些操作好不好,如果tryLock过少,大部分线程都会挂起,因为在拥有对象锁的线程释放锁后不能及时感知,导致用户态和核心态状态转换较多,效率低下,极限思维就是:没有自旋,所有线程挂起,如果tryLock过多,存在两个问题:1. 即使自旋避免了挂起,但是自旋的代价超过了挂起,得不偿失,那我还不如不要自旋了。 2. 如果自旋仍然不能避免大部分挂起的话,那就是又自旋又挂起,效率太低。极限思维就是:无限自旋,白白浪费了cpu资源,所以在代码中每个自旋和tryLock的插入应该都是经过测试后决定的。
编译期间锁优化
- 锁消除
还是先看一下简洁的代码
1 | public class test { |
sb的append方法是同步的,但是sb是在方法内部,每个运行的线程都会实例化一个StringBuilder对象,在私有栈持有该对象引用(其他线程无法得到),也就是说sb不存在多线程访问,那么在jvm运行期间,即时编译器就会将锁消除,该过程依赖JIT的逃逸分析。
- 锁粗化
将前面的代码稍微变一下:
1 | public class test { |
首先可以确定的是这段代码不能锁消除优化,因为sb是类的实例变量,会被多线程访问,存在线程安全问题,那么访问test方法的时候就会对sb对象,加锁,解锁,加锁,解锁,很显然这一过程将会大大降低效率,因此在即时编译的时候会进行锁粗化,在sb.appends(s1)之前加锁,在sb.append(s2)执行完后释放锁。
总结
引入偏向锁的目的:在只有单线程执行情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令置换ThreadID,之后只要判断线程ID为当前线程即可,偏向锁使用了一种等到竞争出现才释放锁的机制,消除偏向锁的开销还是蛮大的。如果同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的,可以通过-XX:-UseBiasedLocking=false来关闭
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和内核态转换),但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁
重入:对于不同级别的锁都有重入策略,偏向锁:单线程独占,重入只用检查threadId等于该线程;轻量级锁:重入将栈帧中lock record的header设置为null,重入退出,只用弹出栈帧,直到最后一个重入退出CAS写回数据释放锁;重量级锁:重入_recursions++,重入退出_recursions–,_recursions=0时释放锁。
最后放一张摘自网上的一张大图
参考资料
https://juejin.cn/post/7001483226678034439
https://www.cnblogs.com/paddix/p/5405678.html
https://www.cnblogs.com/kundeg/p/8422557.html