前面看过 AQS 的状态和队列。把它放回 ReentrantLock,沿着一次获取和释放走下来,几个方法之间的关系就比较清楚了。这里按 JDK 8 的结构看。
非公平锁的入口
ReentrantLock 内部的 Sync 继承 AQS,FairSync 和 NonfairSync 决定获取策略。默认使用 NonfairSync。
非公平 lock 先尝试把 state 从 0 CAS 成 1,成功后设置当前持有线程。失败才进入 AQS 的 acquire,再由 tryAcquire 尝试一次。
tryAcquire 主要检查两种情况:state 为零时尝试获取;state 不为零但持有者就是当前线程时,增加重入次数。其他线程持有时返回失败。
重入不需要重新到队尾等自己释放锁,否则同一线程嵌套加锁就会死锁。
失败后的排队
主要调用关系可以简化成:
lock
-> 尝试直接获取
-> acquire
-> tryAcquire
-> addWaiter
-> acquireQueued
-> 前驱是 head 时再次尝试
-> 安排前驱的通知状态
-> park 等待head 是同步队列的头节点,通常不再代表一个等待中的业务线程。线程成功取得锁后,会把自己的节点设置成新的 head,并清掉线程引用等信息。
等待前要先安排好通知关系,再检查和挂起。这样即使释放与准备等待发生交错,也能通过状态检查和许可机制协调,而不是只写一句 park 就不管了。
取消的节点还需要跳过或断开,实际源码比上面的主线更长,主要是在处理中断、超时和并发变化。
公平策略在哪里
FairSync 在尝试从空闲状态取得锁之前,会检查是否存在排在前面的等待者。NonfairSync 不做相同的先到先得检查,所以刚到的线程可能先拿到刚释放的锁。
这不意味着公平锁上的任何方法都完全相同。无参 tryLock 采用非公平尝试,不遵守这个排队约束。公平性也不是操作系统调度顺序的保证。
释放与重入次数
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantCountDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
lock.lock();
try {
System.out.println(lock.getHoldCount());
} finally {
lock.unlock();
}
System.out.println(lock.getHoldCount());
} finally {
lock.unlock();
}
System.out.println(lock.isLocked());
}
}输出 2、1、false。unlock 会调用 release,由 tryRelease 减少 state。只有减到零,才算完全释放,清除持有者并在需要时唤醒后继。
被唤醒的线程还要重新竞争,unpark 不等于直接把锁交到它手上。普通 lock 等待时即使收到中断,也不会像 lockInterruptibly 那样抛出异常退出;AQS 会记录相应情况,成功获取后恢复中断状态。
看源码时,把 state、持有线程、等待节点三者分开:state 描述资源,持有者用于重入和所有权检查,队列用于协调尚未拿到资源的线程。