synchronized 的语义是互斥和相应的内存可见性,但 JVM 不必每次都用同样昂贵的方式实现。Java 8 HotSpot 会根据竞争情况采用不同的处理方式。
先看字节码
public class MonitorDemo {
private final Object lock = new Object();
public void block() {
synchronized (lock) {
System.out.println("block");
}
}
public synchronized void method() {
System.out.println("method");
}
public static void main(String[] args) {
MonitorDemo demo = new MonitorDemo();
demo.block();
demo.method();
}
}javac MonitorDemo.java
javap -c -v MonitorDemo同步代码块能看到 monitorenter 和 monitorexit,异常路径也会安排释放监视器。同步方法则主要通过 ACC_SYNCHRONIZED 标记表示,不是必须在方法体里看到同样一对指令。
偏向、轻量级与重量级
偏向锁针对同一线程反复进入同一监视器的情况。对象偏向某条线程后,可以减少没有竞争时重复获取的成本。其他线程访问时,可能需要撤销偏向,或者在满足条件时重新偏向。
轻量级锁会通过线程栈上的锁记录和对象头之间的协调来获取锁,关键更新使用 CAS。竞争没有很快解决时,可能膨胀为使用 ObjectMonitor 的重量级锁。
重量级锁支持线程阻塞和唤醒。阻塞避免长期空转,但会带来调度开销;实现也可能在阻塞前做自适应自旋,而不是一遇竞争就立刻挂起。
“无锁、偏向、轻量、重量”是便于理解的状态分类,不是每个对象都必须严格走完的四个步骤。不同参数、身份哈希、竞争和等待操作都会影响路径,也不要把“只升级不降级”当成对象整个生命周期内绝无例外的定律,空闲监视器还涉及回收和脱膨胀。
观察对象头
可以沿用前一篇的 JOL,分别在同步块前、同步块内和退出后打印同一个对象:
import org.openjdk.jol.info.ClassLayout;
public class LockLayoutDemo {
public static void main(String[] args) {
Object lock = new Object();
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}想观察偏向行为,可以在 Java 8 下结合 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 做实验。对象头中的具体数值会随运行环境和对象状态变化,观察时主要看锁标记及其变化。
上面只有一个线程,没有锁竞争。观察竞争下的膨胀时,需要让一个线程持锁,再让另一个线程尝试获取同一把锁。
锁消除与锁粗化
JIT 如果确认某个锁对象不会逃逸到其他线程,可能消除没有实际竞争意义的同步。连续的小范围加锁,也可能被合并成较大的范围,减少反复获取的开销,这叫锁粗化。
测试锁的开销时需要留意这些优化。比如只在方法内部创建并使用的对象,它的锁可能已经被消除,测到的耗时就不能代表发生竞争时的加锁成本。