ReentrantLock浅析

98 阅读7分钟

AQS 抽象队列同步器,是Java面试的热点问题,比如synchronized和ReentrantLock(基于AQS)之间的区别、AQS队列中节点的状态类型、AQS的加锁、解锁过程等等,这篇文章就简单地分析一下AQS的一个主要子类ReentrantLock。

ReentrantLock

为了深入了解一个底层实现,我们先从它的使用方式开始着手,在实践中我们经常会通过ReentrantLock来进行加锁和解锁,对应的lock和unlock两个方法,其实现如下:

public void lock() {  // 获取锁 阻塞
    sync.lock();  // 内部包含AQS.acquire(1)
}
public void unlock() {  // 释放锁
    sync.release(1);  // 内部包含AQS.release(1)
}
public boolean tryLock() {  // 尝试获取锁 非阻塞 直接返回结果
    return sync.nonfairTryAcquire(1);
}

从以上代码可以看出,加锁和解锁的过程都是通过调用sync来实现的,那它是个什么玩意?我们从ReentrantLock的属性和构造函数中可以发现,Snyc是ReentrantLock的一个继承了AQS的静态内部类,并且有两个子类FairSync和NonFairSync。默认情况下ReentrantLock会构造一个非公平的Sync。

private final Sync sync;
public ReentrantLock() {  // 空参的实例化方法
    sync = new NonfairSync();  // 实例化一个非公平锁
}
public ReentrantLock(boolean fair) {  // 指定是否公平
    sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer{...}  // 锁的抽象类
static final class NonfairSync extends Sync{...}  // 非公平锁
static final class FairSync extends Sync{...}  // 公平锁

Sync

我们再深入了解一下Sync内部的具体实现,探究它是如何加锁的:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 阻塞加锁 由子类实现
    abstract void lock();  // 由子类去实现具体的加锁过程(公平锁和非公平锁)
    // 非阻塞加锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {  // 如果可以获取资源
            if (compareAndSetState(0, acquires)) {  // CAS进行修改
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {  // 如果资源已经被持有,判断为当前线程
            int nextc = c + acquires;  // 如果是当前线程则更新state
            if (nextc < 0) // overflow  // 如果超过了Integer的最大值,则抛出异常
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    // 释放锁 公平锁和非公平锁都一样
    protected final boolean tryRelease(int releases) {  // AQS#release会调用
        int c = getState() - releases;  // 减 同步状态
        if (Thread.currentThread() != getExclusiveOwnerThread())// 不允许没有持有锁还想释放锁的行为,没有线程lock资源时getExclusiveOwnerThread()会返回null
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {  // 由于是可重入锁,状态减为0时才算释放成功
            free = true;
            setExclusiveOwnerThread(null);  // 更新当前的独占锁
        }
        setState(c);  // 更新state
        return free;
    }
    // 是否独占持有锁
    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }
    ...
}

NonfairSync和FairSync

从以上代码可以发现,主要的lock方法由其子类NonfairSync和FairSync实现,分别代表了非公平锁和公平锁。

static final class NonfairSync extends Sync {  // 非公平锁
    final void lock() {  // 阻塞式的
        if (compareAndSetState(0, 1))  // 非公平锁不管有没有在等待的线程,直接上去拿锁(不讲武德)
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);  // 父类AQS实现
    }

    protected final boolean tryAcquire(int acquires) {  // AQS#acquire会调用
        return nonfairTryAcquire(acquires);
    }
}
static final class FairSync extends Sync {  // 公平锁
    final void lock() {
        acquire(1);  // 父类AQS实现
    }
    
    protected final boolean tryAcquire(int acquires) {  // AQS#acquire会调用
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 当前资源可以获取,但是需要去看是否还有其他线程在等待(非公平锁直接CAS)
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

AQS#acquire(1)和release(1)

在lock方法中可以发现lock中的acquire(1);和unlock中的release(1);这两个方法都是AQS实现的,接下来就继续剖析一下这AQS中的这两个方法:


public final void acquire(int arg) {  // Lock#lock使用
    
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 加锁失败则中断阻塞自己
        selfInterrupt();
}
public final boolean release(int arg) {  // Lock#unlock使用
    if (tryRelease(arg)) {  // 如果释放成功
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 需要将后继者唤醒
        return true;
    }
    return false;
}

由以上代码可以看出,在aquire和release操作时,都会先进行一个tryAcquire和tryRelease,这玩意是干啥的?实际上try这个过程是一个非阻塞的操作,相当于检查是否符合加锁lock或解锁unlock的条件(具体的实现在ReentrantLock.Sync的两个子类中):

  • tryAcquire:
    • 非公平:(如果当前的state==0且CAS修改state为1成功) 或 (state!=0但是当前线程持有锁,修改state即可)
    • 公平:(如果当前的state==0且当前没有前驱线程在等待且CAS修改state为1成功)或 (state!=0但是当前线程持有锁,修改state即可)
  • tryRelease:非公平和公平都一样,就是检测当前线程是不是当前持有资源的独占线程,然后更新对应的state,如果state==0则返回true,如果state>0代表是可重入锁,当前线程还有其他代码块持有锁,因此返回false

实际上在tryLock和tryRelease的过程中,只是进行了state的更新操作,并没有实际更新队列,那么就需要进一步对这个队列进行操作。AQS 详解 | JavaGuide

  • lock时没有竞争到锁,则会加入到等待队列中,并根据前驱节点的状态进行park操作
  • unlock时如果释放了锁,则需要从队列中移除并unpark符合条件的后继节点 image.png

节点的状态

AQS中的节点是有不同状态的:

状态说明
INITIAL值为0,初始状态
CANCELLED值为1,由于超时或中断,该节点被取消。 节点进入该状态将不再变化。特别是具有取消节点的线程永远不会再次阻塞
SIGNAL值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,那么就会通知后继节点,让后继节点的线程能够运行
CONDITION值为-2,节点在等待队列中,节点线程等待在Condition上,不过当其他的线程对Condition调用了signal()方法后,该节点就会从等待队列转移到同步队列中,然后开始尝试对同步状态的获取
PROPAGATE值为-3,表示下一次的共享式同步状态获取将会无条件的被传播下去

AQS加锁过程

我们先分析如果竞争锁失败怎么加入到这个CLH队列中:

public final void acquire(long arg) {
    // 如果没有获取锁 则加入等待队列中
    // 如果加入等待队列失败则中断自己
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

关键在于acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中的addWaiter和acquireQueued,先分析addWaiter:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);  // 创建一个节点,包含当前线程的指针和后继节点为null
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;  // 当前节点的前驱节点为CLH队列的队尾
    if (pred != null) {
        node.prev = pred;  // 先将当前元素的前驱节点设置为prev
        if (compareAndSetTail(pred, node)) {  // CAS更新CLH的尾节点
            pred.next = node;  // 更新前驱节点的后继节点
            return node;
        }
    }
    enq(node);  // CLH没有初始化 或者 有其他线程抢先把自己链到尾部称为新尾部了 那么就会进行enq的操作
    return node;
}
private Node enq(final Node node) {
    for (;;) {  // 一直循环入队
        Node t = tail;  // 得到当前的尾部
        if (t == null) { // 如果队列没有初始化,没有尾部,则进行初始化
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {  // 否则持续找当前最新的尾部,把自己加上去
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

从以上代码可以看出,addWaiter就是新创建一个节点(包含当前线程的指针)将其CAS加入到尾部,并且返回这个节点

接下来分析acquireQueued:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();  // 前驱节点
            if (p == head && tryAcquire(arg)) {  // 如果前驱节点为head并且当前具备持有锁的条件
                setHead(node);
                p.next = null; // help GC 将之前的头节点指向null
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试去持有锁。持有锁成功则将当前节点设置为head节点,然后删除之前的head节点。

如果加锁失败或者Node的前置节点不是head节点,就会通过shouldParkAfterFailedAcquire方法将head节点的waitStatus变成SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park() 挂起当前线程。此时当前线程需要等待其他线程释放锁来唤醒。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;  // 得到前置节点的等待状态
    if (ws == Node.SIGNAL)  // 如果是SIGNAL状态,则放心地park自己
        return true;
    if (ws > 0) {  // 如果前驱节点是CANCELLED状态,则需要一直向前遍历前,将CANCELLED状态的节点删除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {  // 将前驱节点的等待状态变更为Node.SIGNAL,这样前驱节点释放锁时需要唤醒后继节点
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {  // 通过该方法park自己
    LockSupport.park(this);
    return Thread.interrupted();
}

AQS解锁过程

当线程持有锁时,指向它的节点肯定是头节点,那么现在释放锁的话就需要判断是否需要唤醒后继节点,主要通过unparkSuccessor方法进行唤醒操作。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

当当前头节点不处于初始状态时,代表它需要unpark后继节点:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)  // CAS更新当前节点的状态,使其使出初始状态
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {  // 如果下一个节点为null或者处于cancelled状态
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)  // 从尾节点向前遍历,找到head后面第一个不处于canceled状态的节点
            if (t.waitStatus <= 0)  // 不断更新s的值
                s = t;
    }
    if (s != null)  // 找到符合条件的线程则unpark它
        LockSupport.unpark(s.thread);
}

在以上代码中有一个值得注意的点,就是unparkSuccessor为什么从后往前遍历找第一个符合条件的线程唤醒,对应的代码为for (Node t = tail; t != null && t != node; t = t.prev),可以看出是从tail开始向前遍历的。

unparkSuccessor为什么从后往前遍历找第一个符合条件的线程唤醒

# Java AQS unparkSuccessor 方法中for循环为什么是从tail开始而不是head

这就和入队的插入顺序有关了,在我们竞争锁没有竞争过的时候需要创建一个节点,并将它置于CLH队列中。在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是在if语句块中的代码并没有使用任何手段来保证线程安全!在高并发情况下,可能会出现这种情况:线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。就是还没来得及设置之前tail的后继节点就进行上下文切换了,而当前t.next还是为null,因此会出现后续节点被漏掉的情况。

总的来说,其最根本的原因在于:node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况

private Node enq(final Node node) {
    for (;;) {  // 一直循环入队
        Node t = tail;  // 得到当前的尾部
        if (t == null) { // 如果队列没有初始化,没有尾部,则进行初始化
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {  // 否则持续找当前最新的尾部,把自己加上去
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}