偷偷告诉你,synchronized不再使用偏向锁啦

本文详细解析了Java中Synchronized的锁升级过程,包括从偏向锁到轻量级、重量级锁的转变,以及为何在JDK15后偏向锁被废弃,重点分析了其可维护性差、优化效果不明显和前景不明朗的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Synchronized 底层优化是Java面试常考察的点。简略来说,Synchronzied 采用逐步升级的加锁机制来降低加锁成本,每次加锁失败就升级竞争下一级锁,每升高一级,加锁成本就会提高。

在 Java 中,加锁就是线程用锁对象记录自己的一个标记。当其他线程加锁扫描到锁对象已被其他线程标记,就会加锁失败。不再执行后续临界区的代码。

一、Synchronized锁升级过程

Synchronized 首先尝试获取偏向锁(Biased lock),偏向锁会把对象头中的线程编号字段指向自己,表示锁对象被该线程标记。如果指向失败,则撤销偏向锁,升级为轻量级锁(Lightweight Lock)。这时线程使用 CAS 把对象头交换到自己的栈。如 CAS 失败,会进行重试(即自旋 Spin),重试多次仍然失败,升级到重量级锁(Heavyweight Lock),交给 OS 内核进行最终的同步处理。

二、偏向锁被废弃

偏向锁只需要设置对象头的几个字段的值就能完成加锁,“性能很高”。但在 JDK15 中,偏向锁被默认关闭。在 JDK18 中,更被标记为废弃,并不再允许通过命令行手动开启。
看起来效果很好的偏向锁,为什么逐步被打入“冷宫”?OpenJDK 开发团队在 JEP 374 中解释:

过去看到的性能提升如今远没有那么明显。许多受益于偏向锁的程序都是较旧的遗留程序,它们使用早期的 Java 集合 API,它们在每次访问时进行同步(例如 Hashtable 和 Vector )。对于较新的程序,针对单线程场景通常使用 Java 1.2 中引入的非同步集合(例如 HashMap 和 ArrayList ),针对多线程场景,则使用 Java 5 引入的性能更高的并发结构。这意味着,如果代码升级到这些较新的类,相比偏向锁,会有更大的性能提升。此外,围绕线程池队列和工作线程构建的应用程序通常在禁用偏向锁定的情况下性能更好。(例如,SPECjbb2015 就是这样设计的,而 SPECjvm98 和 SPECjbb2005 则不是)。
偏向锁定的代价是在发生锁争用时需要执行昂贵的撤销操作。因此,受益于它的程序只是那些无竞争同步操作的程序。偏向锁的高效是假定在执行简单的锁检查加上偶尔昂贵的撤销成本,仍然低于执行 CAS 指令的成本。但 HotSpot 已经发生了很大的变化,原子指令成本的变化也改变了保持该关系所需的无竞争操作的数量。
另一个值得注意的方面是,当同步操作上花费的时间只占程序总工作负载的一小部分时,即使先前的成本关系成立,程序也不会从偏向锁中获得明显的性能改进。

三、废弃原因分析

综合来看,偏向锁被废除主要有下面的原因:

3.1 可维护性差

JEP 374 中写到:

偏向锁在同步子系统中引入了大量复杂的代码,并且还会侵入其他 HotSpot 组件

为了实现偏向锁,在 JVM 中引入了大量代码。并且,一个理想的代码库,应该做到“高内聚、低耦合”,但偏向锁的代码和各个模块交叉耦合,相互影响。复杂的偏向锁实现给 OpenJDK 开发者造成了很大的负担,最终让他们不得不考虑放弃。

3.2 优化效果不好

OpenJDK 开发者统计到同步操作在程序实际运行中消耗的资源较少,即使偏向锁有一定提升,但对总体性能影响不大。另外,随着 JDK 并发数据结构(ConcurrentHashMap、CopyOnWriteArrayList等)和 CAS 操作的性能提升,偏向锁的作用很有限

3.3 前景不好

偏向锁优化效果不好,并且也没看到未来的优化空间。没有产出,也没有预期的产品很难长期存在。

四、参考资料

### Java 中 `synchronized` 关键字相关的机制 #### 偏向 (Biased Locking) 偏向旨在减少无竞争情况下的同步开销。当一个线程访问同步块并获取时,JVM会假设该只会被这个线程再次获取。因此,在第一次获得之后,后续对该的操作几乎没有任何成本。 - **工作原理**: JVM会在对象头的标记字段中存储首次定此对象的线程ID[^1]。 - **特点**: - 加和解无需额外消耗资源。 - 如果发生争用,则需要撤销偏向模式,转换成其他形式的。 - 对于单一线程频繁调用的情况特别有效。 ```java public class BiasedLockExample { private final Object monitor = new Object(); public void performAction() { synchronized(monitor) { // 初始状态下monitor处于未定状态;一旦某个线程获得了它,就会成为偏向 System.out.println(Thread.currentThread().getName()); } } } ``` #### 轻量级 (Lightweight Locking) 轻量级用于处理短时间内的轻微并发冲突。在这种情况下,多个线程可以轮流快速地占有同一把而不会造成严重的性能损失。 - **工作原理**: 当检测到有第二个线程试图进入临界区时,当前持有的会被提升至轻量级级别,并且尝试通过循环比较交换(CAS)的方式让后来者取得所有权而不必挂起自己[^4]。 - **特性**: - 非阻塞性质使得等待中的线程能够继续运行而不是立即停止。 - 若长时间无法得到则可能退化为重量级。 - 更适合那些预计会有短暂延迟但又确实会发生少量竞态条件的应用场合。 ```java // 这里展示的是伪代码逻辑, 实际上由JVM内部实现 if (!isLocked()) { acquireInExclusiveMode(); } else if (canBeUpgradedToLightWeight()) { upgradeAndTryAcquireViaCAS(); } else { escalateToHeavyWeightLock(); } ``` #### 重量级 (Heavyweight Locking) 这是最传统的互斥机制,通常意味着较高的上下文切换代价以及较低的整体效率。然而对于某些特定的任务来说却是必要的选择。 - **运作方式**: 所有的请求都将排队等候直至前序事务完成释放为止。在此期间任何新的参与者都得被迫休眠直到轮到了它们才行[^5]。 - **属性**: - 可能导致更高的CPU利用率因为涉及到操作系统层面的过程调度。 - 不过也正因为如此才得以保证绝对的安全性和顺序一致性。 - 主要应用于长期占用共享资源的情形下。 ```java class HeavyWeightLockDemo { static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args)throws InterruptedException{ Thread t1=new Thread(()->{try{lock.lock();System.out.println("Thread 1 holds the lock");Thread.sleep(20);}finally{lock.unlock();}}); Thread t2=new Thread(()->{try{lock.lock();System.out.println("Thread 2 holds the lock");}finally{lock.unlock();}}); t1.start();t2.start(); t1.join();t2.join(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值