乐于分享
好东西不私藏

从源码拆解ReentrantLock:为什么它能成为Java并发锁的首选?

从源码拆解ReentrantLock:为什么它能成为Java并发锁的首选?

你有没有过线上服务突然出现线程阻塞,排查半天发现是锁竞争导致的?ReentrantLock作为Java并发包中最常用的显式锁,它的内部到底是如何实现线程调度的?本文将带你深入JDK源码,拆解ReentrantLock的核心原理。

一、ReentrantLock到底是什么?为什么要使用它?

ReentrantLock是java.util.concurrent.locks包下的显式锁实现类,相比synchronized关键字,它提供了更灵活的锁操作能力:支持公平/非公平锁切换、可中断锁获取、超时锁获取、多条件变量绑定等特性。在高并发场景下,合理使用ReentrantLock可以有效优化线程调度效率,避免不必要的上下文切换。

很多开发者在使用ReentrantLock时,只知道它可以用来解决线程安全问题,但并不清楚它的底层是如何实现锁的获取、排队和唤醒的。今天我们就从JDK 1.8的源码入手,一步步拆解ReentrantLock的核心执行流程。

二、从lock()方法入手:非公平锁的源码执行全流程

我们平时使用ReentrantLock时,最常用的就是lock()和unlock()方法。我们先从lock()方法开始分析:

// ReentrantLock.java
public void lock() {
    sync.lock();
}

这里的sync是ReentrantLock的内部抽象类Sync的实例,根据构造方法参数的不同,sync会被实例化为NonfairSync(非公平锁,默认)或者FairSync(公平锁)。我们先来看默认的非公平锁实现:

// NonfairSync.java
final void lock() {
    // 第一步:直接通过CAS尝试修改锁状态
    if (compareAndSetState(0, 1))
        // CAS成功,设置当前线程为独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // CAS失败,调用AQS的acquire方法
        acquire(1);
}

这段代码是非公平锁的核心体现:不管当前等待队列中是否有排队的线程,新调用lock()的线程都会先尝试直接抢占锁,而不是直接进入队列排队。这也是非公平锁吞吐量更高的原因,但会存在线程饥饿的可能性。

接下来我们看acquire(1)方法,这个方法是AQS(AbstractQueuedSynchronizer)的核心方法之一:

// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法的逻辑可以分为三步:
1.  调用tryAcquire(arg)再次尝试获取锁
2.  如果获取失败,将当前线程封装为Node节点加入等待队列
3.  调用acquireQueued方法让线程在队列中自旋等待,直到获取锁或者被中断

我们先看tryAcquire方法的非公平实现:

// NonfairSync.java
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// Sync.java
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 再次尝试CAS修改锁状态
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入锁判断:如果当前线程已经是独占线程,增加锁状态
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

这段代码实现了可重入锁的核心逻辑:如果当前线程已经持有锁,那么再次获取锁时只需要增加锁的状态值,而不需要重新排队。

如果tryAcquire失败,就会调用addWaiter方法将当前线程封装为Node节点加入等待队列:

// AbstractQueuedSynchronizer.java
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速入队
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速入队失败,调用enq方法自旋入队
    enq(node);
    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;
            }
        }
    }
}

最后是acquireQueued方法,这是线程在队列中等待的核心逻辑:

// AbstractQueuedSynchronizer.java
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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断是否需要挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这个方法会让线程在队列中自旋等待,只有当节点的前驱是头节点时,才会再次尝试获取锁。如果获取失败,就会挂起当前线程,等待被前驱节点唤醒。

三、公平锁和非公平锁的核心差异

很多开发者会混淆公平锁和非公平锁的区别,其实它们的核心差异就在于抢锁的时机:
非公平锁:新线程调用lock()时,会先尝试直接抢锁,失败后再进入队列排队
公平锁:新线程调用lock()时,会先判断等待队列中是否有排队的线程,如果有就直接进入队列排队

公平锁的lock方法实现如下:

// FairSync.java
final void lock() {
    acquire(1);
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平锁多了一步判断:队列中是否有前置节点
        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;
}

可以看到,公平锁的tryAcquire方法多了一个hasQueuedPredecessors()判断,这个方法会检查当前队列中是否有比当前线程更早的等待线程。如果有的话,就不会尝试抢锁,直接进入队列排队。

四、unlock()方法的源码解析:锁是如何释放的?

说完了锁的获取,我们再来看锁的释放流程。unlock()方法的代码如下:

// ReentrantLock.java
public void unlock() {
    sync.release(1);
}

这里的release方法是AQS的方法:

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

release方法会先调用tryRelease方法尝试释放锁,如果释放成功,就会唤醒队列中的后继节点。

tryRelease方法的实现如下:

// Sync.java
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 校验当前线程是否是独占线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果锁状态为0,说明完全释放了锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

这段代码的逻辑很简单:减少锁的状态值,如果状态值为0,就清空独占线程的引用,返回true表示锁完全释放。

如果锁完全释放,就会调用unparkSuccessor方法唤醒后继节点:

// AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从队尾向前查找有效的后继节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这个方法会找到队列中第一个有效的后继节点,唤醒它的线程,让它重新尝试获取锁。

六、常见误区与最佳实践

在使用ReentrantLock时,有几个常见的误区需要注意:
1.  忘记释放锁:ReentrantLock不会自动释放锁,必须在finally块中调用unlock()方法,否则会导致其他线程永远无法获取锁。
2.  过度使用可重入锁:虽然可重入锁很方便,但过度嵌套会增加锁的状态值,增加锁释放的复杂度。
3.  公平锁的性能问题:公平锁虽然可以避免线程饥饿,但会增加大量的队列操作,吞吐量会比非公平锁低很多,除非业务场景要求严格的线程执行顺序。
4.  正确使用条件变量:ReentrantLock可以绑定多个Condition对象,用于实现更复杂的线程通信场景,但必须在获取锁之后才能调用Condition的await()和signal()方法。

七、总结

ReentrantLock作为Java并发包中的核心组件,其内部实现依赖于AQS队列和CAS操作。通过本文的源码拆解,我们可以看到:
– 非公平锁通过先尝试直接抢锁的方式提高了吞吐量,但可能导致线程饥饿
– 公平锁通过严格的队列顺序避免了线程饥饿,但牺牲了部分性能
– 可重入性通过记录锁的持有线程和状态值实现
– 锁的释放流程会唤醒后继节点,让队列中的线程重新尝试获取锁

掌握ReentrantLock的核心原理,可以帮助我们在高并发场景下更好地优化线程调度,解决实际开发中的线程安全问题。如果你在使用ReentrantLock时遇到过什么问题,欢迎在评论区留言交流!