synchronized与Lock底层实现对比

synchronized 关键字和 Lock 接口(及其实现类,如 ReentrantLock)都是 Java 中实现线程同步的机制,但它们在底层实现、性能特性、功能和灵活性上存在显著差异。核心区别在于 synchronized 是 JVM 内置的、基于监视器锁(Monitor)实现的隐式锁机制,而 Lock 是 Java API 层面实现的、基于抽象队列同步器(AQS)的显式锁机制

以下是它们底层实现的主要区别:

  1. 实现层级与机制:

    • synchronized
      • JVM 内置原语: 由 JVM 直接在字节码层面实现和管理。编译器在编译 synchronized 修饰的方法或代码块时,会在其入口和出口分别插入 monitorentermonitorexit 字节码指令。
      • 基于 Monitor(监视器锁): 每个 Java 对象(包括 Class 对象)在 JVM 内部都有一个与之关联的、隐藏的内部锁,称为 监视器锁 (Monitor)对象头锁 (Object Header Lock)。这个 Monitor 是 JVM 内部的一个数据结构(通常由 C++ 实现),它负责管理锁的状态(锁定/未锁定)和等待队列(Entry Set, Wait Set)。monitorenter 指令尝试获取对象的 Monitor,monitorexit 指令释放它。
      • 锁状态存储在对象头: 对象在堆内存中的布局包含一个 对象头 (Object Header)。对象头的一部分(Mark Word)就用来存储与锁相关的信息,如锁标志位、偏向线程 ID、轻量级锁指针、重量级锁指针等。
    • Lock (以 ReentrantLock 为例):
      • Java API 实现: 完全在 Java 代码层面实现(java.util.concurrent.locks 包中)。ReentrantLock 等锁类是 Java 类库的一部分,不依赖特殊的 JVM 字节码指令。
      • 基于 AQS (AbstractQueuedSynchronizer): Lock 接口的核心实现(如 ReentrantLock, ReentrantReadWriteLock, Semaphore, CountDownLatch 等)都依赖于一个强大的框架——抽象队列同步器 (AQS)
      • AQS 核心:
        • 状态变量 (state): 一个 volatile int 变量,表示锁的状态(例如,对于 ReentrantLockstate=0 表示未锁定,state>0 表示被某个线程重入锁定的次数)。
        • FIFO 线程等待队列 (CLH 变体): 一个双向链表实现的队列,用于管理那些未能立即获取到锁而需要等待的线程。
        • CAS (Compare-And-Swap) 操作: 核心的原子操作(通常依赖 sun.misc.Unsafe 类或 Java 9+ 的 VarHandle),用于高效、无锁地更新 state 变量和队列指针。CAS 是构建非阻塞同步算法的基础。
  2. 锁的获取与释放:

    • synchronized
      • 自动获取与释放: 锁的获取和释放由 JVM 自动管理。进入同步块获取锁,退出同步块(包括正常退出和异常退出)释放锁。程序员无法控制获取锁的过程。
      • 阻塞式: 如果锁被其他线程持有,当前线程会无条件的阻塞(进入 Entry Set 等待),直到锁被释放。线程无法中断在 synchronized 块上的等待(除非被 interrupt() 且代码中检查中断并退出)。
      • 锁优化: JVM 会根据竞争情况实施一系列锁优化(见下文)。
    • Lock
      • 显式获取与释放: 必须显式调用 lock() 方法获取锁,并在 finally 块中显式调用 unlock() 方法释放锁。这增加了灵活性但也带来了忘记释放锁的风险。
      • 灵活的获取方式:
        • lock(): 阻塞直到获取锁(类似 synchronized)。
        • tryLock(): 尝试非阻塞获取锁。成功返回 true,失败立即返回 false
        • tryLock(long time, TimeUnit unit): 尝试限时阻塞获取锁。在指定时间内成功返回 true,超时返回 false
        • lockInterruptibly(): 可中断地获取锁。在等待锁的过程中,如果线程被中断,会抛出 InterruptedException 并停止等待。
      • AQS 队列管理: 当线程调用 lock() 且锁不可用时:
        1. 尝试使用 CAS 快速设置 state 获取锁(快速路径)。
        2. 如果失败,将当前线程包装成一个节点(Node),并使用 CAS 操作安全地加入到 FIFO 等待队列的尾部。
        3. 线程进入阻塞状态(通常调用 LockSupport.park()),让出 CPU。
        4. 当持有锁的线程调用 unlock() 释放锁时(通常将 state 减到 0),它会唤醒(LockSupport.unpark())队列头部的等待线程(公平锁)或任意一个等待线程(非公平锁,默认)。
        5. 被唤醒的线程再次尝试获取锁。
  3. 锁的公平性:

    • synchronized 仅支持非公平锁。 当锁被释放时,任何尝试获取锁的线程(包括新来的线程和在队列中等待的线程)都有机会竞争,新来的线程可能“插队”成功。这可能导致某些等待线程饥饿,但通常能提高吞吐量。
    • **Lock (如 ReentrantLock): ** 支持公平锁和非公平锁(默认非公平)。 创建锁时可指定公平策略:
      • 公平锁 (new ReentrantLock(true)): 严格按照 FIFO 顺序从等待队列中唤醒线程获取锁。新来的线程直接排队。保证无饥饿,但可能降低吞吐量(上下文切换开销)。
      • 非公平锁 (new ReentrantLock()new ReentrantLock(false)): 新来的线程可以直接尝试插队获取锁(在入队前尝试一次 CAS),如果失败才入队。释放锁时,被唤醒的线程和新来的线程竞争。可能提高吞吐量,但可能导致等待线程饥饿。
  4. 锁优化:

    • synchronized JVM 实施了复杂的锁优化策略(锁膨胀过程):
      1. 偏向锁 (Biased Locking): 假设锁通常由同一线程获得。第一次获取时,JVM 将线程 ID 记录在 Mark Word 中(偏向模式)。该线程后续进入/退出同步块只需简单检查,几乎无开销。适用于无竞争或极少竞争的场景。
      2. 轻量级锁 (Lightweight Locking / Thin Lock): 当有轻微竞争(第二个线程尝试获取偏向锁)时,偏向锁撤销,升级为轻量级锁。JVM 在当前线程栈帧中创建锁记录空间(Lock Record),将对象头的 Mark Word 复制到其中(Displaced Mark Word),然后用 CAS 操作尝试将对象头替换为指向锁记录的指针。如果成功,线程获得锁。如果失败(其他线程竞争),自旋尝试一小段时间(自适应自旋)。适用于低竞争、同步块执行快的场景。
      3. 重量级锁 (Heavyweight Locking): 当轻量级锁自旋失败或竞争加剧时,锁膨胀为重量级锁。对象头的 Mark Word 指向操作系统层面的互斥量(Mutex)和关联的 Monitor 对象(包含 Entry Set 和 Wait Set)。未获取锁的线程会阻塞并进入内核态等待,需要操作系统进行线程调度(上下文切换),开销最大。适用于高竞争场景。
    • Lock 本身没有内置类似 synchronized 的多级锁优化(偏向->轻量->重量)。它主要依赖 CAS + AQS 队列 + 自旋/阻塞
      • tryLock() 和快速路径尝试本质上是非阻塞的 CAS。
      • lock() 在入队前或入队后唤醒时,通常会进行有限次数的自旋尝试(具体策略可能因 JVM 和锁实现而异),以减少昂贵的线程阻塞/唤醒开销。如果自旋失败,则调用 LockSupport.park() 让线程进入等待状态(通常是用户态阻塞,但最终可能涉及系统调用)。其优化更侧重于在 API 层面提供灵活性(如 tryLock)和在 AQS 内部高效管理队列。
  5. 条件等待:

    • synchronized 通过 Object.wait(), Object.notify(), Object.notifyAll() 实现条件等待。这些方法必须在持有对象的监视器锁(即 synchronized 块内)调用。一个锁只能关联一个隐式的等待队列(Wait Set)。
    • Lock 通过 Lock.newCondition() 方法显式创建 Condition 对象Condition 提供了 await(), signal(), signalAll() 方法,功能类似 wait/notify,但更灵活:
      • 一个 Lock 可以关联多个 Condition 对象,用于管理不同的等待条件(例如,生产者-消费者问题中的“非满”和“非空”两个条件)。
      • 方法命名更清晰 (await/signal vs wait/notify)。
      • Condition 的实现同样基于 AQS,为每个条件维护一个独立的等待队列。

总结对比表:

特性synchronizedLock (以 ReentrantLock 为例)
实现层级JVM 内置原语 (字节码 monitorenter/monitorexit)Java API 实现 (基于 AQS)
核心机制对象监视器锁 (Monitor) / 对象头 Mark WordAQS (状态变量 state + FIFO 等待队列 + CAS)
锁获取/释放隐式、自动显式 (lock()/unlock())
锁获取方式阻塞式阻塞 (lock())、非阻塞 (tryLock())、可中断 (lockInterruptibly())、限时 (tryLock(time))
公平性仅非公平可配置 (公平锁或非公平锁)
锁优化有 (偏向锁 -> 轻量级锁 -> 重量级锁)无内置多级优化,依赖 CAS + 队列 + 自旋/阻塞策略
条件变量一个锁关联一个隐式等待队列 (wait/notify)一个锁可关联多个显式 Condition (await/signal)
编码简洁,不易出错 (自动释放)更灵活,需小心防止忘记 unlock() (必须用 finally)
性能 (历史)JDK 1.5 及之前较差,之后优化后差距缩小JDK 1.5 引入时在高竞争下性能更好,现在两者差距不大
中断响应阻塞等待锁时不可中断阻塞等待锁时可中断 (lockInterruptibly())

选择建议:

  • 优先考虑 synchronized 当同步需求简单,不需要 Lock 提供的那些高级特性(如可中断、超时、公平锁、多个条件)时。它的代码更简洁,由 JVM 自动管理,不易出错。现代 JVM 的优化使其性能在大多数常见场景下足够好。
  • 考虑使用 Lock 当需要以下特性之一时:
    • 可中断的锁获取。
    • 尝试非阻塞或限时获取锁 (tryLock)。
    • 需要公平锁策略。
    • 需要多个关联的条件变量 (Condition)。
    • 需要在不同方法中加锁和解锁(跨越多个作用域)。
    • 进行非常复杂的同步控制(结合多个同步组件)。

理解底层实现的差异有助于你根据具体应用场景(竞争程度、性能要求、功能需求)做出更合适的选择。在不需要 Lock 的高级特性时,简洁可靠的 synchronized 通常是首选。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

走过冬季

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值