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 如果确认某个锁对象不会逃逸到其他线程,可能消除没有实际竞争意义的同步。连续的小范围加锁,也可能被合并成较大的范围,减少反复获取的开销,这叫锁粗化。

测试锁的开销时需要留意这些优化。比如只在方法内部创建并使用的对象,它的锁可能已经被消除,测到的耗时就不能代表发生竞争时的加锁成本。

最后修改:2018 年 05 月 08 日
如果觉得我的文章对你有用,请随意赞赏