源码解析ReentrantLock加锁过程

816 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

相信对并发有所了解的同学对于ReentrantLock并不陌生。没错,今天的主角就是可重入锁ReentrantLock

非公平锁

ReentrantLock的无参构造默认就会创建一个非公平锁

public ReentrantLock() {
    sync = new NonfairSync();
}

我们来看看他是如何加锁的

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
  • 首先是通过CAS设置state值为1;
  • 哪个线程设置成功了,就将此线程设置成独占锁线程,也就是此线程获得锁;
  • 否则执行acquire(1)。
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我们先分析tryAcquire(arg)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)子方法,最后在回过头来分析acquire(int arg)方法

先看tryAcquire(arg)方法

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}
  • 首先获取state的值,判断是否为0,也就是判断是否有线程占用锁;
  • 如果没有线程占用锁,当前线程通过CAS尝试设置state值(尝试获取锁),如果成功返回true;
  • 如果当前线程和独占锁的线程是同一个线程,则将state增加acquires(可重入),并返回true;
  • 否则返回false。

总结下tryAcquire(arg)方法的作用:尝试获取锁,获取到了锁返回true,否则返回false。

addWaiter(Node.EXCLUSIVE)方法主要是做了一个线程入队的操作,这里就不细讲;

主要来看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法。

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);
    }
}

可以看到这个方法的主体内容就是一个死循环。

  • 首先获取当前线程的node节点的上个节点
  • 上个节点如果是头节点,尝试获取锁;
  • 如果获取到锁,设置当前节点为头节点,并将之前的头节点出队;
  • 如果上述判断有一个为false,执行shouldParkAfterFailedAcquire(p, node)方法
  • 如果shouldParkAfterFailedAcquire方法返回true,则调用park方法堵塞当前线程;
  • 如果返回false,则继续循环。

来看看shouldParkAfterFailedAcquire(p, node)到底做了什么

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  • 判断上个节点的waitStatus
  • 如果=-1,返回true;
  • 如果>0,则死循环去找到上上个节点(直到waitStatus<=0为止),返回false;
  • 否则将上个节点的waitStatus设置为-1,返回false。

实际上,shouldParkAfterFailedAcquire(p, node)方法就是判断是否需要堵塞当前线程,并修改上个节点的waitStatus的值为-1,表示上个节点的线程处于休眠状态。

最后,我们来看看真正堵塞的方法parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

就是调用park方法进行线程的堵塞,也就是说线程执行到这就会释放CPU,停在了这,等待被唤醒。

公平锁

公平锁绝大地方跟非公平锁的实现没啥区别,无非是在CAS获取锁之前判断了一下等待队列中的线程是否轮到了当前线程(hasQueuedPredecessors()方法)

总结

到此,ReentrantLock加锁过程就聊完了,这里简单做个总结。

  • 首先通过CAS设置state= 1成功就是获取到锁;
  • 失败则再次尝试获取锁;
  • 如果还是失败,则将这个线程节点加入等待队列;
  • 如果这个节点的上个节点是头节点会再次尝试获取锁,如果成功即获取锁,并进行节点链表的新旧头节点的出入队列操作;
  • 如果失败则会调用park进行堵塞,等待被唤醒。

文中如有不足之处,欢迎指正!一起交流,一起学习,一起成长 ^v^