Java虚拟机同步的核心优化手段锁升级和锁消除,分别通过动态调整锁强度和干掉无用锁来减少并发开销,让synchronized在多数场景下性能逼近无锁。
很多人一听到synchronized,就想起“重量级锁”“性能差”,其实在HotSpot虚拟机的长期打磨下,synchronized早已不是当年那个笨重的家伙,锁升级和锁消除这两招,就是JVM在并发同步上做的“精装修”,先看锁升级,它解决的是“锁太贵”的问题;再看锁消除,它解决的是“锁根本不需要”的问题。
锁升级过程:一把锁的成长之路
锁升级不是一次性到位,而是边走边看,JVM为synchronized设计了从偏向锁到轻量级锁,再到重量级锁的四个状态,无锁、偏向锁、轻量级锁、重量级锁,每一步都在权衡“抢锁的成本”和“等待的时间”。
偏向锁:锁的“初恋”假象
偏向锁的意思是,锁会“偏向”第一个获取它的线程,如果同一个线程再次进入同步块,JVM只需要检查线程ID是否匹配,不需要任何CAS原子操作,更不会触发系统调用,这个状态适合绝大多数场景:单线程反复进入同一个同步块,锁名存实亡。
偏向锁的优化思路很直接:既然没人跟你抢,那锁就“你的名字,如果偶尔有另一个线程来竞争,偏向锁会撤销并升级,撤销需要等待一个全局安全点,这反而有一点小开销,所以JVM提供了延迟偏向和批量重偏向机制,就是为了应对“撤销太频繁”的尴尬。
轻量级锁:CAS自旋的“白领博弈”
当第二个线程真的来竞争时,偏向锁让位,JVM进入轻量级锁模式,此时两个线程都在用户态,通过CAS尝试把对象头的Mark Word替换成指向自己线程栈中的锁记录,成功者拿到锁,失败者进入自旋,也就是反复用CPU时间换上下文切换的时间。
自旋不是无止境的,适配器会动态调整自旋次数,如果一直抢不到,就膨胀为重量级锁,这里的性能关键点在于:短时间的锁竞争,自旋比挂起线程更划算,行业共识认为,Java 6之后的synchronized之所以性能大幅提升,正是这套升级逻辑在发挥作用。
重量级锁:不得不“惊动内核”
如果竞争持续激烈,轻量级锁扛不住,锁就升级为重量级锁,这个状态下,获取锁的线程会进入操作系统内核,被挂起或唤醒,发生用户态和内核态的切换,成本较高,但这也是为了公平和避免CPU空转的不得已之选。
整个升级过程不可逆一旦升级到重量级锁,就不会再降级,所以实战中要尽量避免代码里出现“高并发、长时间持锁”的同步块,否则锁会直接进入重量级,前面的优化全部白费。
锁消除和锁粗化:JVM的“抠门”哲学
锁消除和锁粗化是JIT编译器在运行时做的优化,和锁升级不同,它们不改变锁的状态,而是直接调整“锁是否存在”或者“锁的粒度”。
锁消除:白加的锁,直接扔掉
如果你在方法内部创建了一个对象,这个对象从头到尾都没有逃逸出方法,也就是没有其他线程能“看见”它,那么JVM通过逃逸分析认定这个锁是多余的,直接消除锁,最常见的例子是,在任何方法里用StringBuffer拼字符串,或者把Vector当作局部变量使用。
public String buildString(String a, String b) {
StringBuffer sb = new StringBuffer();
sb.append(a);
sb.append(b);
return sb.toString();
}
这里每个线程调用时都会新建自己的StringBuffer,不存在共享,锁自然被JVM干掉,相关JVM参数是-XX:+EliminateLocks,在JDK 8及之后默认开启,但要注意,逃逸分析只有在服务端模式(-server)下才会充分生效,C1或解释执行时优化力度有限。
锁粗化:别频繁开关锁,合并成一次
锁粗化反过来,它会扩大锁的范围,想象你在循环里一次次进入同步块,每次加锁解锁都是有成本的,JIT发现这些锁其实是同一把锁,而且前后接连出现,干脆把整个循环体都用一把锁包住,减少锁的反复获取和释放。
for (int i = 0; i < 100; i++) {
synchronized(lock) {
count++;
}
}
JIT可能会优化成:
synchronized(lock) {
for (int i = 0; i < 100; i++) {
count++;
}
}
锁粗化不是鼓励你把同步块写得很大,而是针对“循环中反复加锁”这种反模式做兜底,如果代码本身只在必要的小范围内加锁,JVM自然用不上粗化,反过来,如果你刻意把一个巨大的同步块放那,编译器可不会帮你拆小。
Java锁升级和锁消除区别是什么?从原理到实战
不少开发者把这两个概念混在一起,其实它们的目标和实现路径完全不同,把区别理清楚,面试和调优时都不容易翻车。
| 对比项 | 锁升级 | 锁消除 |
|---|---|---|
| 优化对象 | 正在使用的锁 | 没必要存在的锁 |
| 锁的状态 | 偏向锁→轻量级→重量级 | 直接移除锁操作 |
| 触发条件 | 多线程竞争强度变化 | 对象不逃逸出线程 |
| 成本代价 | 撤销偏向锁有安全点开销 | 需要依赖逃逸分析 |
| 适用场景 | 单线程与多线程交错 | 方法内私有对象 |
实战中,锁升级是“兵来将挡”,根据不同竞争级别用不同的同步策略;锁消除是“没有敌人不开枪”,直接省掉弹药,两者并不冲突,可能同时发生在同一段代码里,比如一个局部StringBuffer,既可能因为不逃逸而被消除锁,也可能因为循环中的缓存逻辑而参与锁粗化,JVM会按照编译优化启发式自行决策。
JVM锁优化面试题高频考点:怎样回答才能说到点上
如果你在准备面试,重点不是背结论,而是理解JVM做决策的“价值取向”,下面几个问题基本覆盖了相关考点。
偏向锁和轻量级锁的性能差别有多大?
偏向锁省去了CAS原子操作,只做一次内存比较,代价几乎为零,轻量级锁至少需要一次CAS,多数情况下一次CAS就能成功,重量级锁则涉及系统调用,成本可能高出几个数量级,但偏向锁撤销需要安全点,所以不能迷信“偏向一定最快”。
锁消除依赖什么技术?
锁消除依赖逃逸分析,JVM通过分析对象的作用域,确认对象不会被其他线程访问,才敢去掉锁,逃逸分析本身有成本,所以在解释执行模式下通常不做,JIT编译后才触发,这也能解释为什么同样的代码,跑热了以后性能反而更好。
锁粗化会带来什么负面影响?
锁粗化扩大了临界区,可能导致线程持锁时间变长,反而增加其他线程的等待时间,但JVM只在确认相邻锁操作之间没有其他线程介入时才粗化,所以实际危害很小,如果同步块本身极短,粗化通常利大于弊。
业内专家指出,理解锁优化不能只看单个机制,要把“无竞争时的偏向、轻竞争时的自旋、重竞争时的阻塞”串起来看,面试时能把这条逻辑讲通,比记住一堆参数更有价值。
关于锁升级与锁消除的常见问题
锁消除和锁粗化是相反的吗?
从结果上看方向相反,一个减少锁,一个扩大锁,但出发点一致:消除不必要的同步开销,锁消除针对“完全无私”的对象,锁粗化针对“频繁开关”的锁,两者都由JIT触发,且可能同时作用于不同代码段。
为什么现在的synchronized性能不输ReentrantLock?
synchronized经过锁升级优化后,在低竞争场景下开销极小;高竞争时重量级锁经过自适应自旋和偏向锁撤销优化,也不像老版本那么糟糕,ReentrantLock提供更丰富的功能,比如可中断、超时、公平锁,但单纯比较同步开销,两者差距已经非常小,多数业务场景选择synchronized,是因为它简单可靠且自动释放锁。
如何用JVM参数验证锁优化是否生效?
可以开启-XX:+PrintEliminateLocks(JDK 8中常用)观察锁消除的日志,或者用-XX:+PrintSafepointStatistics查看偏向锁撤销带来的安全点暂停,生产环境建议先测试再调整,不要盲目关掉任何默认优化,锁升级和锁消除是JVM帮你打工,你只需要把代码写得结构清晰,让逃逸分析和锁粒度控制更容易奏效就行。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/614371.html





