锁是怎么实现的
在Java中,锁通过控制线程对共享资源的访问权限来实现线程的阻塞与等待。其核心机制涉及对象监视器(Monitor)和队列管理,以下是具体实现原理及流程:
一、内置锁(synchronized)的线程等待机制
1. 对象监视器(Monitor)模型
每个Java对象都与一个Monitor关联,由对象头中的Mark Word记录锁状态。
- Mark Word结构:包含锁标志位(如01表示无锁,00表示轻量级锁,10表示重量级锁)和指向Monitor的指针。
- Monitor核心组件:
- Entry Set(等待队列):线程尝试获取锁失败后进入此队列,处于阻塞状态(BLOCKED)。
- Owner(持有者):当前持有锁的线程。
- Wait Set(等待集合):线程调用
wait()后释放锁并移入此队列,等待notify()唤醒。
2. 线程阻塞与唤醒流程
- 加锁(monitorenter):
- 线程尝试通过CAS修改Mark Word获取锁。
- 若锁已被占用,线程进入Entry Set并阻塞(操作系统级互斥量Mutex实现)。
- 释放锁(monitorexit):
- 持有锁的线程执行完毕或抛出异常时释放锁。
- JVM从Entry Set唤醒一个线程重新竞争锁。
示例:
synchronized (obj) {
// 临界区代码
}
编译后对应字节码monitorenter和monitorexit指令,控制线程的阻塞与唤醒。
二、显式锁(ReentrantLock)的线程等待机制
显式锁基于AQS(AbstractQueuedSynchronizer)实现,通过内部队列和状态变量管理线程阻塞。
1. AQS核心组件
- state变量:表示锁的占用状态(0未锁定,≥1被占用,支持可重入)。
- CLH队列:双向链表结构的等待队列,存储阻塞线程的节点(Node)。
2. 线程阻塞与唤醒流程
- 加锁(lock()):
- 尝试通过CAS修改
state字段获取锁。 - 若失败,将线程包装为Node加入CLH队列尾部,并调用
LockSupport.park()挂起线程。
- 尝试通过CAS修改
- 释放锁(unlock()):
- 持有锁的线程修改
state为0。 - 唤醒队列中首个等待节点(通过
LockSupport.unpark())。
- 持有锁的线程修改
示例:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
三、锁的线程等待策略对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 阻塞机制 | 重量级锁依赖操作系统互斥量 | 基于AQS自旋+CAS,减少上下文切换 |
| 灵活性 | 不支持中断或超时 | 支持tryLock(timeout)和可中断锁 |
| 公平性 | 非公平锁(默认) | 可选公平/非公平模式 |
四、线程等待的底层实现
1. 操作系统级阻塞
- 重量级锁(synchronized):依赖内核态互斥量(Mutex),线程切换涉及用户态到内核态转换,开销较大。
- 轻量级锁优化:通过CAS自旋减少内核态切换,若自旋超过阈值(默认10次)则升级为重量级锁。
2. 线程状态变化
- BLOCKED:线程竞争锁失败后进入阻塞状态(如Entry Set中的线程)。
- WAITING:调用
wait()后线程进入等待集合(Wait Set),需显式唤醒。
五、避免线程饥饿与死锁
- 顺序加锁:多个线程按相同顺序获取锁,避免循环依赖。
- 超时机制:使用
tryLock(timeout)防止无限等待。 - 死锁检测:通过工具(如
jstack)分析线程堆栈,定位锁竞争路径。
总结
Java通过Monitor模型(synchronized)和AQS队列(ReentrantLock)实现线程的阻塞与唤醒。
- synchronized:简单易用,但灵活性差,适合低竞争场景。
- ReentrantLock:支持高级功能(如中断、超时),适合复杂并发需求。
理解底层机制有助于优化高并发程序性能,避免死锁等问题。

浙公网安备 33010602011771号