synchronized 与 ReentrantLock:Java 锁机制原理与实现对比

synchronized 与 ReentrantLock:Java 锁机制原理与实现对比

synchronized 与 ReentrantLock:Java 锁机制原理与实现对比

目录

  • 为什么有两种锁
  • synchronized 的本质
  • JVM 锁优化
  • ReentrantLock 的设计
  • AQS 的核心机制
  • 功能对比
  • 实战:线程安全的缓存
  • 小结

为什么有两种锁

Java 提供了两种锁机制:synchronized 和 ReentrantLock。很多开发者写并发代码时,会习惯性地用 synchronized,因为语法简单,加个关键字就行。但翻看一些成熟的开源项目,会发现大量使用 ReentrantLock 的地方。

两者的核心区别在于抽象层级不同。synchronized 是 JVM 内置的,编译器自动在字节码中插入加锁和解锁指令,开发者不需要关心锁的获取和释放过程。ReentrantLock 是 JDK 在 API 层面提供的锁实现,需要手动调用 lock() 和 unlock(),但换来的是更丰富的功能:可中断、可超时、公平锁、多条件变量。

这篇文章从字节码和 AQS 的层面拆解两者的实现原理,搞清楚它们各自的设计思路,以及什么时候该用哪个。

synchronized 的本质

synchronized 看起来只是一个关键字,但在字节码层面,它会被编译成两条指令:monitorentermonitorexit

publicvoidsyncMethod(){synchronized(this){// 临界区代码}}

编译后的字节码大致是这样:

monitorenter // 尝试获取 this 对象的 Monitor // 临界区字节码 monitorexit // 释放 this 对象的 Monitor monitorexit // 异常退出时也需要释放(编译器自动插入)

注意有两个 monitorexit:第二个是编译器为了异常安全自动插入的,确保即使临界区抛出异常,锁也能被释放。这就是为什么 synchronized 不需要手动释放锁,JVM 会帮你兜底。

这里有个关键概念:synchronized 锁的是对象的 Monitor 结构。每个 Java 对象都可以作为 Monitor 的关联对象,HotSpot 会在对象头(Mark Word)中记录锁状态信息。当 synchronized 竞争某个对象时,JVM 会通过 Monitor 机制管理线程同步。Monitor 是一种逻辑结构,不一定在对象创建时就存在,而是在发生锁竞争时才被关联。

synchronized 有三种用法,锁的对象不同:

// 1. 实例方法 — 锁的是当前实例对象 thispublicsynchronizedvoidinstanceMethod(){}// 2. 静态方法 — 锁的是 Class 对象(类级别的锁)publicstaticsynchronizedvoidstaticMethod(){}// 3. 代码块 — 锁的是括号里指定的对象publicvoidblockMethod(){synchronized(lockObject){}}

实例方法锁 this,静态方法锁 Class 对象,代码块锁你指定的对象。同一个对象的 Monitor 在同一时刻只能被一个线程持有,其他线程想获取就得排队。

JVM 锁优化

早期的 synchronized 直接就是重量级锁,依赖操作系统的 Mutex Lock,涉及用户态到内核态的切换,开销很大。JDK 6 之后,JVM 引入了锁升级机制,根据竞争程度动态调整锁的状态,把锁的开销降了下来。

在早期 HotSpot 中,锁升级分三个阶段:偏向锁 → 轻量级锁 → 重量级锁。但从 JDK 15 开始,偏向锁默认关闭,JDK 18 的 HotSpot 已经彻底移除了偏向锁。现在更准确的描述是轻量级锁和重量级锁之间的动态调整。

无锁状态 │ ▼ (线程进入 synchronized 块) 轻量级锁 │ ▼ (自旋失败,竞争激烈) 重量级锁

轻量级锁:线程在用户态通过 CAS 自旋尝试获取锁,不涉及内核态切换。自旋几次之后通常就能拿到锁,因为大多数锁的竞争窗口很短。

重量级锁:如果自旋多次还是拿不到锁,轻量级锁会膨胀为重量级锁。此时线程会被阻塞,进入 Monitor 的 EntryList 等待队列,涉及用户态到内核态的切换,开销明显增大。

锁膨胀通常不是简单的状态切换,JVM 会根据竞争情况动态调整。过去版本中常描述为"锁升级不可逆",但现代 HotSpot 的实现更加复杂,不能简单理解为永远只能升级。

锁状态Mark Word 内容适用场景开销
轻量级锁指向栈中锁记录的指针低竞争、短临界区CAS 自旋
重量级锁指向 ObjectMonitor 的指针高竞争线程阻塞、内核切换

ReentrantLock 的设计

ReentrantLock 是 JDK 在 API 层面提供的锁,位于 java.util.concurrent.locks 包下。它不依赖 synchronized 的 monitorenter/monitorexit 指令,而是基于 AQS,通过 CAS 和 LockSupport 实现线程竞争、阻塞和唤醒。

ReentrantLocklock=newReentrantLock();lock.lock();try{// 临界区代码}finally{lock.unlock();// 必须在 finally 中释放}

和 synchronized 相比,最大的区别是unlock() 必须手动调用。忘记释放锁会导致死锁,标准写法是把 unlock() 放在 finally 块里。这是 ReentrantLock 的一个使用门槛,换来的是更灵活的控制能力。

ReentrantLock 提供了几个 synchronized 做不到的特性:

可中断等待:synchronized 的线程一旦进入阻塞状态,只能傻等。ReentrantLock 支持在等待锁的过程中响应中断。

ReentrantLocklock=newReentrantLock();Threadt=newThread(()->{try{lock.lockInterruptibly();// 可中断地获取锁// 正常业务逻辑}catch(InterruptedExceptione){System.out.println("等待锁的过程中被中断了");}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}});t.start();// 主线程在 1 秒后中断子线程Thread.sleep(1000);t.interrupt();

超时获取:tryLock 支持设置等待时间,超时后自动放弃,不会无限等待。

if(lock.tryLock(5,TimeUnit.SECONDS)){try{// 拿到锁,执行业务}finally{lock.unlock();}}else{// 5 秒内没拿到锁,走降级逻辑System.out.println("获取锁超时,执行降级方案");}

公平锁:synchronized 的锁竞争是非公平的,刚来的线程可能插队抢到锁。ReentrantLock 支持公平模式,严格按照等待顺序分配锁。

ReentrantLockfairLock=newReentrantLock(true);// 公平锁

公平锁保证了先来后到的顺序性,但吞吐量会低于非公平锁,因为每次都要从队列头部取出等待线程,不能直接竞争。大多数场景下用默认的非公平锁就行。

AQS 的核心机制

ReentrantLock 的底层是 AQS(AbstractQueuedSynchronizer),Semaphore、CountDownLatch、ReadWriteLock 底层也是它。AQS 的核心结构就两样东西:一个 volatile int state + 一个 FIFO 队列

// AQS 的核心字段(简化版)publicabstractclassAbstractQueuedSynchronizer{privatevolatileintstate;// 同步状态privatetransientNodehead;// 队列头节点privatetransientNodetail;// 队列尾节点}

在 ReentrantLock 中,state 表示锁的重入次数。state = 0 是未锁定,state > 0 是锁定且值为重入次数。但 state 在不同同步器中含义不同——Semaphore 中它是剩余许可数量,CountDownLatch 中它是倒计数数量。AQS 只提供框架,具体语义由子类定义。

ReentrantLock 默认是非公平锁(NonfairSync),这也是大多数场景的首选。非公平锁和公平锁的 acquire 流程有一个关键区别:

非公平锁允许插队:刚到的线程不需要检查队列,直接 CAS 抢锁。如果恰好锁空闲,它就拿到了,省去了入队、出队的开销。这种"插队"看似不公平,但在高并发场景下吞吐量更高,因为减少了线程切换的次数。公平锁则严格按 FIFO 顺序,保证没有饥饿,但每次获取锁都要检查队列,性能会低一些。

来看非公平锁的完整获取流程:

release 流程:

release(): state-- if state == 0: // 可重入时减到 0 才真正释放 exclusiveOwnerThread = null LockSupport.unpark(队列中下一个等待线程)

AQS 把锁的竞争和排队逻辑抽象成通用框架。ReentrantLock 只需定义 state 的语义和公平/非公平策略,队列管理、线程阻塞和唤醒这些通用逻辑全交给 AQS。这也是 JUC 包里那么多并发工具都能复用 AQS 的原因。

功能对比

特性synchronizedReentrantLock
实现层级JVM 内置,字节码指令JDK API,AQS + CAS
阻塞机制ObjectMonitorAQS + LockSupport
锁获取/释放自动(进入/退出同步块)手动 lock()/unlock()
可重入支持支持
可中断不支持lockInterruptibly()
超时获取不支持tryLock(timeout)
公平锁不支持new ReentrantLock(true)
条件变量只有 wait/notify多个 Condition
锁状态查询不支持isLocked(), getHoldCount()

synchronized 的优势是简单。关键字级别的语法支持,不用关心锁的释放,JVM 自动保证异常安全。对于"一个方法加锁"这种简单场景,synchronized 写起来更简洁,也更难犯错。

ReentrantLock 的优势是灵活。可中断、可超时、公平锁、多条件变量,这些功能在复杂并发场景下非常有用。比如"尝试获取锁,拿不到就走降级逻辑"这种需求,synchronized 就做不了。

总结

现代 JVM 对 synchronized 的优化已经很深,轻量级锁、锁消除、锁粗化这些技术让两者的性能差异在多数业务场景下并不明显。选择标准是需不需要 ReentrantLock 的额外功能。

简单决策:需要超时获取、可中断等待、多条件变量这三个中的任何一个,用 ReentrantLock;其他场景使用 synchronized 就够了。