Java中的常用锁
在java中,我们主要用到的锁有两个:synchronized、ReentrantLock。一个是内置锁(语言级),一个是显示锁,一般如果我们不思考只图方便的化,会直接使用synchronized锁。其实ReentrantLock更灵活多变,适用跟多的场景。我们可以通过下面的表对比一下两者。
特性/维度 | synchronized |
ReentrantLock |
---|---|---|
所属包 | Java 语言层面(内置关键字) | java.util.concurrent.locks |
可重入性 | ✅ 支持 | ✅ 支持 |
是否必须手动解锁 | ❌ 自动(方法或代码块退出时) | ✅ 必须手动 unlock() |
是否可中断获取锁 | ❌ 不支持 | ✅ 支持 lockInterruptibly() |
是否支持尝试获取锁 | ❌ 不支持 | ✅ 支持 tryLock() (立即或超时) |
是否支持公平锁 | ❌ 不支持 | ✅ 支持(new ReentrantLock(true) ) |
是否支持条件队列 | ❌ 不支持 | ✅ 支持 Condition (类似 wait/notify ) |
性能优化(偏向锁等) | ✅ JVM 自动优化 | ❌ 无(完全由开发者控制) |
是否支持可见调试 | ❌ 无法查看状态 | ✅ 可查看是否被锁定、等待线程等 |
死锁处理 | 不易发现 | 可结合 tryLock() 等策略避免死锁 |
其实在上述这些维度方面都没啥好说的,这些一般都知道。都知道synchronized是关键字而ReentrantLock是一个类,然后两者都能加锁,这锁怎么去加,有什么效果,都是知道的。
那synchronized和ReentrantLock又分别是怎么加锁的呢?
#### synchronized
加锁原理(底层)
- Java 对象头中包含了 Mark Word,在没有加锁时存储哈希值、GC信息等。
- 当线程进入
synchronized
块时,会尝试修改对象头的 Mark Word,以标识当前线程持有锁。 - JVM 使用了多种锁优化策略(偏向锁、轻量级锁、重量级锁):
- 偏向锁:如果只有一个线程访问,尽量不加锁,提高性能。
- 轻量级锁:使用 CAS 尝试获取锁,适用于低竞争。
- 重量级锁(互斥锁):竞争激烈时,会挂起线程,使用
Monitor
来实现,依赖操作系统的互斥机制。
加锁过程简要流程图(逻辑)
1 | 线程进入synchronized: |
字节码层面
编译后使用 javap -v Test.class
查看,会看到如下结构:
1 | java |
反编译字节码:
1 | asm |
monitorenter
和 monitorexit
是 JVM 指令,由 JVM 解释器直接处理,底层是调用 native 方法去操作对象的 Monitor。
JVM实现细节
HotSpot 中,对象的 Monitor 实际上是 ObjectMonitor
:
1 | class ObjectMonitor { |
Monitor 与每个对象的 对象头(Mark Word) 关联,当竞争严重时锁会升级:
- 偏向锁:只记录线程 ID,适合无竞争场景
- 轻量级锁:线程通过 CAS 抢占锁
- 重量级锁:膨胀为
ObjectMonitor
,线程挂起和唤醒都依赖 OS 的park/unpark
ReentrantLock
加锁原理(底层)
ReentrantLock
是基于 AQS(AbstractQueuedSynchronizer) 实现的。- 调用
lock()
会尝试通过 CAS 修改一个 state 状态变量,来标识是否持有锁。 - 若获取失败,线程会被放入 AQS 的 双向等待队列中,等待唤醒。
加锁过程简要流程图(逻辑)
1 | 线程调用lock(): |
AQS的队列结构
1 | head -> [Node1] -> [Node2] -> ... |
每个 Node 包含:
- 等待线程
Thread thread
- 等待状态
waitStatus
- 前驱/后继节点
prev
/next
底层通过 LockSupport.park()
和 unpark()
来实现线程的阻塞与唤醒。
如何唤醒
两者都是有阻塞队列,队列都是有序的,为什么会出现非公平唤醒呢?
**因为”排队” ≠ “唤醒一定按顺序”**。
Java 的锁(尤其是默认的 synchronized
和非公平 ReentrantLock
)使用的阻塞队列只是阻塞等待顺序的组织方式,但唤醒后是否真正获得锁,还要竞争(CAS),所以就可能出现:
先排队的线程被唤醒了,但还没抢到锁;此时一个新线程进来,直接CAS成功拿到了锁。
1. synchronized
的非公平行为
synchronized
底层用的是 ObjectMonitor
,当有多个线程竞争时,JVM 不保证 先唤醒的一定先拿到锁。
1 | CopyEdit |
唤醒后还要重新尝试获取锁(CAS),不是直接转移锁的拥有权,所以:
后来的线程进来,如果没有阻塞,仍然可能直接成功拿到锁。
被唤醒的线程,反而还得和新线程竞争,可能继续失败。
2. ReentrantLock
的非公平行为
默认构造的 ReentrantLock()
是非公平锁,非公平策略体现在 lock()
方法中:
1 | CopyEdit |
也就是说:
- 没有优先检查 AQS 队列中是否有等待的线程
- 只要锁是空闲的,谁来谁抢,先到的不一定先拿到
即使队列中有等待线程,新线程仍然可以“插队”成功。