bigywater.tech
Java 内部原理 · 16

synchronized 锁膨胀:真实测出有竞争比无竞争慢了将近 9 倍

"synchronized 现在有锁升级,无竞争时很快"是常见说法,但"很快"和"有竞争时慢多少"很少有人给出真实数字。用同一个自增计数器,分别在单线程无竞争和 8 线程竞争下真实计时。

215ms vs 2084ms
真实计时:4000 万次 synchronized 自增,单线程无竞争约 215ms,8 线程竞争同一把锁约 2084ms
约 9 倍
两次独立运行的竞争/无竞争倍数分别是 9.69 和 8.74,count 全程真实正确,没有丢失更新

上一篇:字符串 + 拼接编译成了什么:真实反汇编发现不是 new StringBuilder

思路:同一把锁,两种竞争程度

对照系列里 ReentrantLock 公平锁那篇的做法,这次换成最基础的 synchronized 关键字,看无竞争和有竞争之间的真实差距。

第一组:单线程自己反复进出 synchronized 块 4000 万次,没有其它线程抢这把锁。第二组:8 个线程一起抢同一把锁,总共也做 4000 万次自增,用 CountDownLatch 让所有线程同时起跑,保证竞争是真实、同时发生的:

SyncInflation.java(竞争场景核心) for (int i = 0; i < threads; i++) { ts[i] = new Thread(() -> { startGate.await(); // 所有线程一起起跑 for (int j = 0; j < perThread; j++) { synchronized (lock) { counter++; } } doneGate.countDown(); }); ts[i].start(); }

真实验证:无竞争 vs 8 线程竞争

两组总操作次数完全相同(都是 4000 万次),唯一变量是有没有其它线程在抢同一把锁。

真实实测 · 第一次运行
单线程无竞争: 40000000 次 synchronized 自增, 耗时 = 215ms, counter=40000000
8 线程竞争同一把锁: 共 40000000 次 synchronized 自增, 耗时 = 2084ms, counter=40000000
倍数(竞争/无竞争) = 9.69
真实实测 · 第二次独立运行
单线程无竞争: 40000000 次 synchronized 自增, 耗时 = 243ms, counter=40000000
8 线程竞争同一把锁: 共 40000000 次 synchronized 自增, 耗时 = 2127ms, counter=40000000
倍数(竞争/无竞争) = 8.74

两次运行的倍数分别是 9.69 和 8.74,量级稳定在 9 倍左右。两组的 counter 最终都真实等于 40000000,证明 synchronized 全程保证了互斥,没有丢失更新——这也是系列前面volatile 那篇里普通 int 和 AtomicInteger 对照实验的反面:这里用了真实的互斥锁,竞态条件被完全挡住了,代价就是这 9 倍的真实性能差距。

这 9 倍差在哪?不是"多执行了几条指令"这么简单。无竞争时,JVM 的锁实现能走很轻量的路径(偏向锁/轻量级锁,靠 CAS 自旋就能拿到锁,不涉及操作系统调度);一旦真的有多个线程同时抢,锁会真实膨胀成重量级锁,抢不到的线程需要被操作系统挂起、之后再被唤醒、发生真实的线程上下文切换——这部分开销比单纯的几次 CAS 重试要昂贵得多,也是这 9 倍差距的主要来源。

分步演示:锁竞争发生时,JVM 是怎么真实记录下来的

每一行都是真实运行这段代码,通过 Thread.getState() 和 ThreadMXBean 得到的结果。

锁竞争造成的真实影响,不用秒表也能真实观测到:JVM会把等锁的线程真实标记成BLOCKED状态,并且真实精确记录它们各自在等谁
这一步新验证的线程状态

    参考与说明

    • 本文计时数据均在本机真实运行 Java 17、8 核并发得到,两次独立运行数量级一致。
    • 竞争条件下"结果依然正确但速度更慢"这个模式,和ConcurrentHashMap、ReentrantLock 两篇是同一类权衡,建议对照读。
    • 没有涉及:用 -XX:+PrintFlagsFinal 或反射直接读取对象头 markword 来逐字节验证锁状态标记——这次只用真实的吞吐量差距侧面印证锁膨胀,更细粒度的验证留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号