欢迎光临
我们一直在努力

万字长文|synchronized & Lock 核心原理「状态标记 + 队列等待 + 可重入」

在 Java 并发编程中,synchronized(内置锁)和Lock(以 ReentrantLock 为核心的显式锁)是实现线程同步的两大核心手段。本文从底层实现、核心机制、性能特性、使用场景等维度,对两者进行全方位拆解,帮你彻底搞懂其原理与应用逻辑。

关键概念速释

概念核心定义作用
Monitor 每个 Java 对象自带的监视器锁,是 synchronized 的底层载体 管理 synchronized 的锁获取 / 释放 / 阻塞
锁升级 JVM 对 synchronized 的性能优化,锁状态不可逆升级 降低 synchronized 在低并发场景的开销
AQS 抽象队列同步器,是 Lock 体系的 “骨架” 封装通用同步逻辑(排队、阻塞、唤醒)
state AQS 的 volatile int 变量,表征锁 / 资源状态 标记锁是否被占用、重入次数等
CLH 队列 AQS 的双向链表队列 存放抢锁失败的线程,保证 FIFO 排队
CAS CPU 级原子指令(比较并交换) 保证 state / 队列操作的原子性,无锁并发核心

一、核心认知:两者的本质定位

  • synchronized:JVM 层面的内置同步机制,语法简洁、自动管理,无需手动操作,是 Java 开发者最基础的同步工具;
  • Lock(核心为 ReentrantLock):JDK 层面的显式锁框架,基于 AQS 实现,功能灵活、可定制化强,是复杂并发场景的首选。

        无论是 synchronized 还是 Lock,底层都围绕俩个核心逻辑设计:

  • 状态标记:用一个变量标记锁的持有状态(如是否被占用、重入次数);
  • 排队等待:抢锁失败的线程需要进入队列阻塞,避免无限竞争消耗资源;
  • 两者的核心目标一致 ——保证多线程下共享资源的原子性、可见性、有序性,但实现层面、优化策略、功能边界完全不同。

    二、synchronized 核心原理(JVM 内置的 “傻瓜式” 锁)

    1. 底层基石:Monitor(监视器锁)

    synchronized 的核心是对象的 Monitor(监视器) —— 每个 Java 对象都自带一个 Monitor,同步的本质是竞争 Monitor 的所有权。

    Monitor 的 “占有 – 释放” 逻辑

    synchronized 的本质是竞争 Monitor 的所有权,所有同步逻辑围绕 Monitor 展开:

    • 获取锁(monitorenter 指令):
    • 若 Monitor 进入数为 0,线程获取 Monitor 并将进入数设为 1,成为 Monitor 所有者;
    • 若当前线程已持有该 Monitor(重入),进入数 + 1;
    • 若 Monitor 被其他线程持有,当前线程进入阻塞状态,直到 Monitor 被释放。
    • 释放锁(monitorexit 指令):
    • 执行该指令的线程必须是 Monitor 所有者;
    • 进入数 – 1,若减至 0 则释放 Monitor,唤醒阻塞队列中的线程重新竞争。

    注:monitorexit 会在两种场景执行 —— 同步代码块正常执行完毕、代码块抛出异常时,保证锁一定会释放,避免死锁。

    2. 锁升级(从 “低效” 到 “适配场景”):锁升级机制(JVM 的关键优化)

    早期 synchronized 是 “重量级锁”,性能较差;JDK 1.6 后引入锁升级(不可逆),大幅提升性能,锁状态从低到高分为 4 级:

    锁状态适用场景核心实现性能特点
    无锁 无线程竞争 直接操作数据,无加锁开销 无任何性能损耗
    偏向锁 单线程重复获取锁 锁偏向第一个获取的线程,对象头标记 “偏向线程 ID”,后续该线程拿锁无需 CAS 几乎无开销,单线程最优
    轻量级锁 多线程交替获取锁(无持续竞争) 用 CAS 操作抢锁(修改对象头的锁记录指向当前线程),失败则膨胀为重量级锁 低开销,避免内核态阻塞
    重量级锁 多线程持续竞争锁 调用操作系统 Mutex(互斥锁),线程进入内核态阻塞 开销最大,依赖操作系统调度
    • 偏向锁:解决 “单线程重复加锁” 的性能问题,避免每次加锁都走 CAS;
    • 轻量级锁:解决 “少量线程交替竞争” 的问题,用用户态 CAS 替代内核态阻塞;
    • 重量级锁:最终兜底方案,牺牲性能保证线程安全。

    3. synchronized 核心特性

    • 自动加锁 / 释放:无需手动操作,JVM 保证出同步块(或异常)时释放锁;
    • 可重入:同一线程可多次获取同一把锁(Monitor 进入数累加);
    • 非公平锁:默认不保证线程排队顺序,可能 “插队” 抢锁;
    • 不可中断:线程抢锁失败会一直阻塞,无法被主动中断;
    • 可见性:依赖 JVM 的内存屏障,保证锁释放后变量修改对其他线程可见。

    三、Lock(ReentrantLock):JDK 层面的 “灵活式” 锁(以 ReentrantLock 为例)

    Lock 是接口,ReentrantLock 是最核心的实现类,其底层完全依赖AQS(抽象队列同步器) ——AQS 是 Java 并发包的 “骨架”,ReentrantLock 只是 AQS 的 “业务实现”。

    1. AQS(抽象队列同步器)

    AQS 不是锁,而是一套同步模板框架,定义了 “线程抢锁 – 排队 – 唤醒” 的通用逻辑,核心组成:

    • 同步状态(state):volatile int 变量,表示锁的状态(0 = 未锁定,>0 = 已锁定,值 = 重入次数);
    • CLH 同步队列:双向链表,存放抢锁失败的线程,按 FIFO 排队;
    • CAS 操作:CPU 级原子指令,保证 state 修改、队列操作的原子性;
    • 独占 / 共享模式:ReentrantLock 用独占模式(同一时间仅一个线程持有锁)。

    2. ReentrantLock 核心实现逻辑

    (1)锁的获取(lock ())

             tryAcquire () 是核心:分公平锁 / 非公平锁实现,本质是 CAS 修改 state。

    (2)锁的释放(unlock ())

            tryRelease ():state 减 1,只有减至 0 才真正释放锁,保证重入的正确性。

    重入:同一线程拿锁时直接 state+1,释放时 state-1(和 synchronized 逻辑一致)。

    3. 公平锁 vs 非公平锁(ReentrantLock 的核心差异)

    ReentrantLock 默认是非公平锁,可通过构造函数指定公平锁:

    (1)非公平锁(默认)

    抢锁时直接 CAS 修改 state,不管队列中是否有等待线程,允许 “插队”:

    protected final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 直接CAS抢锁,无视队列
    if (c == 0) {
    if (compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
    }
    }
    // 重入:当前线程已持有锁,state+1
    else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
    }
    return false;
    }

    • 优势:吞吐量高(减少线程切换开销);
    • 劣势:不公平,可能导致某些线程长期等待。
    (2)公平锁

    抢锁前先检查 CLH 队列,只有队列为空 / 当前线程是队头,才 CAS 抢锁:

    protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    // hasQueuedPredecessors()检查队列是否有前驱线程
    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
    }
    }
    // 重入逻辑和非公平锁一致
    else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0) throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
    }
    return false;
    }

    • 优势:公平,严格按排队顺序抢锁;
    • 劣势:吞吐量低(频繁切换线程)。

    4. ReentrantLock 核心特性(灵活度拉满)

    • 手动加锁 / 释放:必须在 finally 中调用 unlock (),否则可能死锁;
    • 可重入:和 synchronized 一致,state 记录重入次数;
    • 可中断:支持 lockInterruptibly (),抢锁时可被中断;
    • 超时抢锁:支持 tryLock (long timeout, TimeUnit unit),超时未抢到则返回;
    • 条件变量:支持 Condition,实现线程的精准唤醒(synchronized 只能唤醒全部);
    • 可见性:依赖 volatile state 和 CAS,保证变量修改的可见性。

    四、synchronized vs ReentrantLock 核心对比

    对比维度synchronizedReentrantLock
    底层实现 JVM 层面(Monitor + 锁升级) JDK 层面(AQS+CAS+CLH 队列)
    锁类型 仅非公平锁 公平锁 / 非公平锁(可指定)
    释放方式 自动释放(出同步块 / 异常) 手动释放(必须 unlock ())
    中断支持 不支持 支持(lockInterruptibly ())
    超时抢锁 不支持 支持(tryLock ())
    精准唤醒 不支持(只能唤醒全部) 支持(Condition)
    性能 低并发:和 ReentrantLock 持平;高并发:略差 高并发:性能更优
    使用成本 低(语法简洁,不易出错) 高(需手动释放,易遗漏)
    异常处理 JVM 自动保证释放 需在 finally 中处理,否则死锁

    五、实战选型建议

    优先用 synchronized 的场景

  • 简单同步场景(如单方法 / 代码块同步);
  • 低并发场景(性能足够,且无需复杂功能);
  • 怕出错的场景(自动释放锁,降低死锁风险)。
  • 优先用 ReentrantLock 的场景

  • 复杂并发场景(如需要公平锁、可中断、超时抢锁);
  • 高并发场景(吞吐量要求高);
  • 需要精准唤醒线程(如生产者消费者模型);
  • 需要手动控制锁的释放时机。
  • 六。总结:从原理到应用的核心认知

  • synchronized 和 ReentrantLock 核心都是通过「状态标记 + 队列等待」实现互斥,且都支持可重入;
  • synchronized 胜在 “简单安全”,由 JVM 自动管理;ReentrantLock 胜在 “灵活可控”,支持更多高级特性;
  • 选型核心:简单场景用 synchronized,复杂并发场景用 ReentrantLock,无需过度追求 “性能最优”,优先保证代码安全。
  • 赞(0)
    未经允许不得转载:171主机测评 » 万字长文|synchronized & Lock 核心原理「状态标记 + 队列等待 + 可重入」
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址