1. ReentrantLock与AQS的关系揭秘
第一次接触ReentrantLock时,我像大多数人一样只停留在API调用层面。直到某天排查一个死锁问题,跟踪lock()方法进入源码后,才发现原来它内部藏着如此精妙的设计。ReentrantLock的整个加锁解锁机制,实际上都是委托给一个叫AbstractQueuedSynchronizer(简称AQS)的同步器来实现的。
AQS就像是一个同步框架的"骨架",它定义了获取和释放共享资源的整套机制。而ReentrantLock则是基于这个骨架实现的"具体产品"。这种设计非常巧妙——AQS负责处理线程排队、阻塞唤醒等通用逻辑,而具体的资源获取和释放规则(比如是可重入的还是不可重入的,是公平的还是非公平的)则交给像ReentrantLock这样的实现类自己定义。
在AQS内部,维护着一个volatile int类型的state变量来表示同步状态。对于ReentrantLock来说,state=0表示锁未被占用,state>0表示锁被占用且数值代表重入次数。这种设计既简洁又高效,通过一个字段就同时解决了锁状态和重入次数两个问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从"抢座位"看非公平锁实现
2.1 CAS操作的抢锁机制
记得刚学编程时老师用"抢座位"游戏来解释锁的概念——教室里有且只有一把椅子(共享资源),同学们(线程)要争抢这把椅子。非公平锁的实现就像这个游戏最原始的版本:
java复制final void lock() {
if (compareAndSetState(0, 1)) // 尝试直接抢座位
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到就排队
}
这段代码完美展现了非公平锁的精髓:不管有没有人在排队,新来的线程直接尝试通过CAS(Compare-And-Swap)操作修改state来抢锁。就像迟到的同学不排队,直接冲进教室试图抢椅子。
CAS是CPU提供的原子操作,包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当V的值等于A时,CAS才会将V的值设为B。在x86架构下,CAS对应的是lock cmpxchg指令,通过锁总线或缓存行来实现原子性。
2.2 非公平性的代价与收益
非公平锁的这种设计带来了一个有趣的现象:理论上,新来的线程可能比等待队列中的线程更早获取到锁。这看似不公平,但实际上减少了线程挂起和唤醒的开销,整体吞吐量更高。
在我的性能测试中,在高竞争场景下,非公平锁的性能可以比公平锁高出1-2个数量级。这是因为线程切换的成本很高(通常在微秒级别),而非公平锁减少了上下文切换的次数。
但非公平锁也有缺点——可能导致线程饥饿。在极端情况下,某些线程可能长时间获取不到锁。因此,当业务需要严格的公平性时(比如订票系统),就应该使用公平锁。
3. AQS队列的底层实现剖析
3.1 CLH队列的变体
当CAS抢锁失败后,线程会进入AQS的等待队列。这个队列是基于CLH锁的变体实现的。CLH是Craig、Landin和Hagersten三位学者提出的自旋锁算法,AQS对其进行了改造:
- 将自旋改为阻塞挂起
- 显式维护前驱和后继节点
- 增加了状态位用于控制线程的阻塞和唤醒
队列中的每个节点都是一个Node对象,主要包含以下关键字段:
java复制volatile int waitStatu
