连载中 4/21

synchronized(下):锁升级之路与 wait/notify 两条铁律

2026-10-24 · 82 阅读 · 0 评论 · 0 赞

一把锁的四级进化

早期 synchronized 直接对接操作系统的互斥量,加锁解锁都要陷入内核态——哪怕根本没有竞争。「synchronized 很重」的黑历史就是这么来的。JDK 6 之后引入锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,只升不降,按竞争烈度逐步加码。四级形态的差距,本质是「花多少代价防万一」。

偏向锁:只有一个线程的场合

大多数锁整个生命周期只有同一个线程在访问——那第一次拿到锁后,干脆在 Mark Word 里记下线程 ID,之后这个线程再进来连 CAS 都省了,直接放行。听起来很美,但 JDK 15 起偏向锁已被默认禁用并标记废弃(JEP 374):撤销偏向要安全点停顿、维护成本高、在现在多线程常态化的应用里收益萎缩。老项目的启动参数里还能见到 -XX:+UseBiasedLocking,新代码不该再依赖它。了解它,主要是为了看懂面试题和旧代码。

轻量级锁:CAS 自旋的短跑

第二个线程来了,升级为轻量级锁:线程在自己栈帧里划一块 Lock Record,把 Mark Word 复制进去,再用 CAS 把对象头指向这块记录。抢不到的线程不自挂,原地自旋几圈——赌的是「锁马上就放」。它适合持有时间极短、冲突不激烈的场景;自旋是空耗 CPU 的,转久了得不偿失,于是就有了下一级。

重量级锁:内核态的大枪

自旋若干次还没抢到,膨胀为重量级锁:monitor 对接操作系统互斥量,抢不到的线程被直接挂起(park),让出 CPU——上下文切换进内核态,代价大,但不再空烧 CPU。适合持锁时间长、竞争激烈的场景。synchronized 的聪明之处就在这:根据现场自动选形态,你一行代码都不用改。所以别再背「synchronized 是重量级锁」的老黄历了——那是 JDK 6 之前的故事。

wait/notify 铁律一:必须在 synchronized 里

wait 和 notify 是挂在 Object 上的方法,配套规则第一条:必须在 synchronized 块内调用同一把锁对象的 wait/notify,否则抛 IllegalMonitorStateException。道理也直白:wait 的语义是「我先睡,醒来时之前看到的状态仍然可信」——只有持着锁,这个承诺才成立;不持锁就 wait,唤醒后拿到的状态毫无保障,那还睡个什么劲。

铁律二:等待条件必须写在 while 里

用 wait/notify 写一个最小生产者消费者缓冲区:

class Buffer {
    private final Queue<Integer> data = new LinkedList<>();
    private final int cap = 10;

    public synchronized void put(int v) throws InterruptedException {
        while (data.size() == cap) {   // 必须 while,不能 if
            wait();                     // 释放锁,进入 WAITING
        }
        data.offer(v);
        notifyAll();                    // 唤醒所有等待者重新判断
    }

    public synchronized int take() throws InterruptedException {
        while (data.isEmpty()) {
            wait();
        }
        int v = data.poll();
        notifyAll();                    // 消费完,唤醒等空间的另一边
        return v;
    }
}

为什么是 while 不是 if?因为 wait 返回不代表条件成立:你可能是被 notifyAll 唤醒的十个线程之一,条件早被手脚快的抢完了;也可能只是碰上一次虚假唤醒。醒后重走判断,条件不成立就继续等,这才是完整语义。那为什么用 notifyAll 而不是 notify?notify 只随机喊醒一个,而所有等待者共用一个等待队列——万一喊醒的是「等错条件」的那个(比如等空间的被喊来消费),该醒的没醒,全体永久等待,经典死锁。notifyAll 多唤醒几个看起来浪费,但正确性永远排在性能前面。

synchronized 篇完。下一篇进入 Java 内存模型的主战场:为什么线程 A 改了变量,线程 B 就是看不见——volatile 登场。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Java#synchronized#锁升级#wait-notify#JUC

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10113 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9876 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9306 阅读 · 21 评论 · 287 赞