前面看过 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 描述资源,持有者用于重入和所有权检查,队列用于协调尚未拿到资源的线程。

最后修改:2018 年 05 月 20 日
如果觉得我的文章对你有用,请随意赞赏