在 Java 中,synchronized关键字底层的锁机制在 JDK 1.6 之后经历了重大的优化,引入了“锁升级”(Lock Escalation)的概念 。你提到的这四种锁并不是完全独立的四种机制,而是synchronized在不同竞争激烈程度下,为了平衡性能开销而采取的不同状态或策略。
这里需要先澄清一个核心概念:偏向锁、轻量级锁、重量级锁是对象头(Mark Word)中记录的锁状态,而自旋锁是一种在获取锁失败时,为了避免线程立即阻塞而采用的优化技术。
以下是它们各自的详细介绍以及相互之间的升级联系:
一、 核心概念分别介绍
1. 偏向锁 (Biased Lock)
核心思想:假设一段同步代码在大多数情况下不仅不存在多线程竞争,而且总是由同一个线程多次获得 。
工作原理:当线程第一次访问同步块并获取锁时,会在对象头和栈帧的锁记录中存储该线程的 ID 。以后该线程再进入和退出同步块时,只需检查对象头的线程 ID 是否一致,无需再进行任何底层的 CAS (Compare-And-Swap) 同步操作 。
适用场景:极低并发场景,或者基本上只有一个线程在跑同步块 。
补充说明:由于维护偏向锁在多线程竞争时撤销的成本较高,JDK 15 之后已经默认废弃了偏向锁 。
2. 轻量级锁 (Lightweight Lock)
核心思想:假设多线程会交替执行同步块(即有多个线程,但刚好错开时间,没有发生真正的同时刻碰撞) 。
工作原理:当偏向锁被其他线程访问时,偏向模式被破坏,锁会升级为轻量级锁 。线程在自己的栈帧中创建锁记录(Lock Record),然后通过 CAS 操作尝试将对象的 Mark Word 替换为指向自己锁记录的指针 。如果成功,则获取锁 。
适用场景:线程交替执行同步块,存在极短时间的微弱竞争 。
3. 自旋锁 (Spin Lock) / 适应性自旋
核心思想:线程的阻塞和唤醒需要 CPU 从用户态切换到内核态,开销非常大 。自旋锁认为:“锁被占用的时间通常很短,我不急着去睡觉(阻塞),我先在门外转几个圈(空循环)等一下” 。
工作原理:在轻量级锁竞争失败时,线程不会立即挂起,而是执行一个空的
for循环(自旋) 。如果在自旋期间锁被释放了,该线程就能直接拿到锁,从而避免了昂贵的上下文切换 。适应性自旋:JDK 1.6 引入,自旋的次数不再固定 。如果上次自旋成功拿到了锁,这次就多转几圈;如果上次没拿到,这次就少转甚至不转,直接升级 。
4. 重量级锁 (Heavyweight Lock)
核心思想:最传统的锁,用于应对激烈的多线程碰撞 。
工作原理:依赖于操作系统的
Mutex Lock(互斥量)来实现 。当锁升级为重量级锁后,所有没有拿到锁的线程都会被真正地挂起(阻塞,进入等待队列),放弃 CPU 调度,直到持有锁的线程释放锁后将它们唤醒 。适用场景:高并发、代码块执行时间较长、锁竞争非常激烈的场景 。
二、 它们之间的联系(锁膨胀/升级过程)
这四者是一个动态演进、单向升级的过程,旨在让系统在不同并发压力下都保持最优性能 。整个升级链路如下:
无锁状态 -> 偏向锁当对象刚被创建,且第一个线程进入
synchronized代码块时,对象头的锁标志位变为偏向锁,并记录下该线程的 ID 。此时没有任何额外的同步开销 。偏向锁 -> 轻量级锁当第二个线程尝试获取该对象的锁时,发现偏向锁的线程 ID 不是自己 。此时偏向锁会被撤销(Revoke),并在全局安全点(Safe Point)将锁状态升级为轻量级锁。这两个线程开始通过 CAS 操作竞争锁 。
轻量级锁 + 自旋锁的结合当轻量级锁竞争出现时(比如线程 A 还在执行,线程 B 进来了),线程 B 发现通过 CAS 拿不到锁 。此时线程 B不会立刻升级为重量级锁并阻塞,而是启动自旋锁机制,在 CPU 上空转等待一小段时间 。
轻量级锁 -> 重量级锁如果线程 B 自旋超过了规定的次数,或者在 B 自旋的过程中,又来了一个线程 C 参与竞争,这说明当前的竞争非常激烈 。此时,自旋不再有意义(只会白白浪费 CPU 资源),锁会发生最后一次膨胀,升级为重量级锁。此时,除了拿到锁的线程外,其余所有竞争线程都会被操作系统放入阻塞队列,进入睡眠状态 。
总结:
synchronized默认先给你最轻的偏向锁;如果有别人来凑热闹,就升级为轻量级锁;如果两人撞车了,没抢到的人先用自旋锁原地等一等;如果等不到或者来抢的人太多,彻底撕破脸,升级为需要操作系统介入的重量级锁。