JVM锁升级机制详解,从偏向锁到重量级锁的演进过程
在Java虚拟机(JVM)中,为了提高多线程环境下共享资源访问的效率,设计了一套锁的优化机制,被称为锁升级或锁膨胀。这个过程主要目的是在保证线程安全的前提下,尽可能地减少锁带来的性能开销。整个过程通常是随着竞争情况的发展而逐步升级的,从最轻量级的偏向锁开始,到轻量级锁,最后如果竞争非常激烈,就会升级到重量级锁。这个机制在如HotSpot这样的主流JVM中都有实现。
第一阶段:偏向锁
偏向锁是锁升级的起点,它的设计理念是“偏向于第一个获取它的线程”。根据Java并发编程领域的权威资料(例如《深入理解Java虚拟机》一书中的描述),在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了减少同一个线程重复获取锁的开销,就引入了偏向锁。当一个线程第一次访问同步块时,会在对象头和栈帧中的锁记录里存储偏向的线程ID。之后这个线程再进入和退出同步块时,不需要进行复杂的加锁解锁操作,只需要简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,可以快速执行。这就像给这个线程开了个“绿色通道”,只要没有其他线程来竞争,它就独享这个通道,速度很快。偏向锁适用于几乎没有竞争的场景,它的假设是“锁会一直被同一个线程使用”。
第二阶段:轻量级锁
但是,如果另一个线程也来尝试获取这个锁,情况就变了。偏向锁的假设被打破。根据JVM内部机制的描述,此时偏向锁需要被撤销。撤销过程需要等待一个全局安全点(在这个时间点,所有线程都暂停执行),然后检查持有偏向锁的线程是否还活着或者是否还在同步块中。如果原线程已经释放了锁或者不活跃了,锁可以重新偏向给新的线程。但如果存在竞争,即两个或以上的线程交替执行同步块,但并没有同时争抢,JVM就会将锁升级为轻量级锁。轻量级锁的加锁过程是通过CAS(Compare-And-Swap,一种乐观锁操作)来完成的。线程会在自己的栈帧中创建一个称为锁记录(Lock Record)的空间,用于拷贝对象头的Mark Word,然后尝试用CAS将对象头的Mark Word替换为指向这个锁记录的指针。如果成功,当前线程就获得了锁。如果失败,表示有其他线程也在竞争,这时会进行自旋(就是循环尝试CAS)来尝试获取锁。轻量级锁的理念是,假设竞争是短暂的,通过让线程“稍等一下”(自旋)来避免直接陷入操作系统层面的阻塞,因为那种阻塞和唤醒线程的操作成本很高。这种锁适合竞争时间短、线程交替执行的场景。
第三阶段:重量级锁
如果竞争加剧,情况就不同了。比如,当一个线程自旋等待锁超过一定次数(自旋次数有阈值控制),或者等待的线程数量超过一个,轻量级锁就会膨胀为重量级锁。根据对JVM源码和实现的常见解读(例如在OpenJDK社区的相关文档中提及),重量级锁是依赖操作系统底层的互斥量(Mutex Lock)来实现的。这个过程中,对象头的Mark Word会指向一个监视器对象(Monitor),这个监视器对象管理着一个等待队列。当一个线程获取锁失败时,它会被操作系统挂起(进入阻塞状态),并被放入等待队列中。直到持有锁的线程释放锁后,操作系统会从等待队列中唤醒一个线程来尝试获取锁。这种阻塞和唤醒涉及到操作系统内核态和用户态的切换,开销非常大,所以被称为“重量级”。重量级锁适用于高并发、长时间竞争的场景,它虽然慢,但能有效管理大量线程的竞争,避免无休止的自旋消耗CPU资源。
总结演进过程
总的来说,JVM的锁升级是一个从乐观到悲观、从低成本到高成本、逐步适应竞争强度的动态过程。它始于偏向锁,目标是消除无竞争时的同步开销;一旦出现竞争,就转向轻量级锁,试图用自旋等待避免昂贵的阻塞;当竞争变得激烈,自旋成为负担时,最终升级为重量级锁,通过操作系统的机制来管理线程调度。这个机制体现了在软件设计中一种重要的权衡思想:根据实际情况动态选择最合适的策略,而不是一直使用代价最高的方案。了解这个过程,对于编写高效、并发安全的Java程序非常有帮助。