Skip to main content

锁的优化和升级

编译与运行时优化(JIT 介入)

锁消除

很多时候锁不是主动加的,而是 Java 的历史遗留API或者框架帮你加的。最典型的例子就是 StringBuffer

锁消除的核心依赖于 逃逸分析(Escape Analysis。 在编译代码时,JIT 编译器会分析新创建的对象的引用范围。如果一个对象满足以下条件:

  • 只在当前线程的方法内部被使用。
  • 没有作为返回值返回,也没有被赋值给全局变量或成员变量(即没有“逃逸”到方法外部)。

既然这个对象永远不可能被其他线程访问到,那么针对这个对象加的同步锁(synchronized)就是完全多余的。JVM 就会大胆地把锁操作连根拔起

private static String concatString(String s1, String s2) {
// sb 是方法内部的局部变量,且没有逃逸出这个方法
StringBuffer sb = new StringBuffer();

// StringBuffer 的 append 方法源码内部带有 synchronized 关键字
// 逃逸分析发现 sb 不逃逸,JIT 会在编译时直接把这里的锁全部“消除”
sb.append(s1);
sb.append(s2);

return sb.toString();
}

锁粗化

场景:原则上我们写代码要求同步块“越小越好”,但如果代码中对同一个对象频繁地加锁、解锁, 甚至在循环体里加锁,频繁的同步操作反而会带来严重的性能损耗。

原理:JVM 探测到一连串零碎的操作都在对同一个对象加锁时,会把锁的范围扩展(粗化)到整个操作序列的外部

private static void doSomethingCombined() {
// 【锁粗化前】:这里有 4 个独立的 synchronized 块
// 【JVM 优化后】:JIT 会直接把这 4 个锁合并,只在最外面加一次锁、最后解一次锁
synchronized (LOCK) {
counter++;
}
synchronized (LOCK) {
counter++;
}
synchronized (LOCK) {
counter++;
}
synchronized (LOCK) {
counter++;
}
}

锁升级机制(Lock Inflation)

对象的锁状态记录在对象头(Object Header)的 Mark Word 里 无锁 ➔ 偏向锁 ➔ 轻量级锁 ➔ 重量级锁

偏向锁(Biased Locking)

  • 目的:偏向锁是为了在无竞争情况下提高性能而引入的。它的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式, 当这个线程再次请求锁时,将会无需进行同步。
  • 工作机制当锁对象第一次被线程获得时,虚拟机会在对象头中存储偏向锁的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作, 只需要简单地检查对象头的Mark Word里是否存储着指向当前线程的偏向锁。
  • 适用场景:单一线程频繁执行同步块操作。
  • biased lock

轻量级锁(Lightweight Locking)

  • 目的:轻量级锁设计用来优化多线程竞争锁的场景,但竞争的程度不是特别激烈。它减少了传统锁在操作系统中的开销。
  • 工作机制:当一个线程尝试获取锁时,如果锁对象没有被锁定(即无偏向锁),JVM会在当前线程的栈帧中创建一个锁记录(Lock Record),然后尝试使用CAS将对象头的MarkWord更新为指向锁记录的指针。如果成功,当前线程获得锁。如果有另一个线程已经持有了锁, 那么当前线程尝试自旋等待锁释放。
  • 适用场景:线程交替执行同步块操作。

自旋锁

后面请求锁的线程暂时不放弃处理器的执行时间,看持有锁的线程是否很快释放锁,让当前线程执行一个忙循环
默认自旋10次
减少线程挂起和唤醒的开销

自适应自旋锁

由上一次在同一个锁的自旋时间以及拥有者的状态来决定的,如果上次获取成功,这次自旋就可以多自旋几次,比如100, 如果上次很少获取成功,则以后可能直接省略掉自旋过程

减少开销和保护cpu之间取得平衡

重量级锁(Heavyweight Locking)

  • 目的:在有激烈竞争的情况下,为了减少线程之间的竞争,将锁的控制权交给操作系统的Mutex Lock来管理,每个重量级锁都会关联一个系统级别的互斥量。
  • 工作机制当轻量级锁尝试失败后(即自旋失败),锁就会膨胀为重量级锁。此时,如果有线程试图获取锁,将会阻塞并进入操作系统的等待队列,直到锁被释放。
  • 适用场景:多线程竞争激烈,且长时间持有锁。