ReentrantLock源码通读(一)

ReentrantLock的基本构成

ReentrantLock实现了两个接口,分别是Lock, Serializable,实现Lock接口,ReetrantLock所有与Lock接口相关各种功能代码,其实都由内部抽象类Sync实现,抽象类Sync继承AbstractQueuedSynchronizer。

ReentrantLock内部有两个继承Sync抽象类的子类,分别是NonfairSync(非公平锁),FairSync(公平锁),它们之间实现的区别决定了线程获取锁的顺序。使用ReetrantLock的构造函数public ReentrantLock(boolean fair)决定ReentrantLock是公平锁还是费公平锁,如果使用无参构造函数,使用NonfairSync非公平锁。

如何实现可重入

ReentrantLock被称为重入锁,特点是同一个线程是可以重复申请锁的,这种可以重复申请锁的机制的好处就是可以避免死锁。主要实现在NonfariSync与FairSync中tryAcquire(int acquires)中,重入锁机制的代码实现没有差别,这里摘出FairSync的相关代码展示:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //省略无关代码
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        //如果int值溢出,将会抛出错误
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //省略无关代码
}

当前线程已持有锁,在重复请求锁时会在getState()的值上再加上acquires,更新锁的持有次数,并返回。

释放锁时,会调用Sync中tryRelease(int releases):

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

只有当getState()获取到的值减去releases等于0时,才会将持有线程设置为null,并返回true,表示锁完全释放。

严格使用ReentrantLock中的API,acquires和releases的值都始终是1,那么ReentrantLock被线程调用tryLock(),返回结果为true几次,就需要调用unlock()几次,否则使用重入锁会造成新的死锁问题。

公平锁与非公平锁实现

ReentrantLock实质上只是一个包装类的效果,所以直接着重看Sync类,比较重要的有抽象方法lock()tryRelease(int releases)tryRelease(int releases)加了final修饰关键字,其它方法基本上都被final修饰,禁止子类重写。

NonfariSync与FairSync区别

NonfairSync与FairSync只重写lock()tryAcquire(int acquires),NonfairSync与FairSync之间,这两个方法的区别:

//NonfairSync实现
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

//FairSync实现
final void lock() {
    acquire(1);
}

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

当线程使用ReentrantLock请求锁时:

NonfairSync会直接CAS操作请求锁,失败时才让当前线程进入等待线程队列中。
FairSync在等待线程队列中没有其他元素时才使用CAS操作请求锁,否则直接进入等待线程队列。

lock()方法类似,两者差别主要在CAS操作获取锁之前的判断条件上:

//tryAcquire两者不一致代码展示
//NonfairSync实现
if (c == 0) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

//FairSync实现片段
if (c == 0) {
    if (!hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

FairSync仅仅是比NonfairSync多一个!hasQueuedPredecessors()条件:

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t && 
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

hasQueuedPredecessors()就是检查等待线程队列中,是否还有其他的等待线程。

FairSync一定是按照先后申请顺序让线程持有锁吗?
不一定是公平的,使用ReentrantLock竞争锁的方法有三个分别是lock()tryLock()tryLock(long timeout, TimeUnit unit)lock()tryLock(long timeout, TimeUnit unit)最终会调用sync的tryAcquire(int acquires),使用FairSync,它会老老实实检查队列,最后加入队列。而tryLock()却没有调用tryAcquire(int acquires),它会调用sync的nonfairTryAcquire(int acquires)

小结:NonfairSync某些特殊时刻无视竞争线程队列,直接尝试让当前线程直接持有锁,而FairSync则强制遵循先进先出的原则,按照排队顺序,一个一个顺序持有锁。当FairSync尝试tryLock()方法请求锁时需要注意,它不是一个公平竞争锁的方法。

ReentrantLock请求锁流程

这个请求锁的流程控制是在AbstractQueuedSynchronizer中实现的,理想流程可以直接简化为sync.tryAcquire(1),这里尝试分析在多个线程请求锁的情况下流程代码,可以分解为几个步骤:进入等待队列 > 请求锁 > 成功/失败/异常 > 释放锁。下面按照这个流程,分步骤分析源码。

进入等待队列

当直接尝试请求锁失败时,将会让当前线程加入到等待线程队列当中:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //直接将之前的tail节点赋值给pred,当前node将会代替之前的tail节点
    Node pred = tail;
    //这里检查是否为空是因为tail和head节点一开始不会初始化
    if (pred != null) {
        //队列尾加入新节点
        node.prev = pred;
        //尝试CAS方式替换尾节点为node,成功之后,为
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //当队列没有初始化时以及CAS方法失败时调用这个方法
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //当head和tail未初始化
        if (t == null) {
            //初始化head和tail后接着循环
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //死循环,并且不停的尝试CAS将tail节点设置为node
            //head节点不会有prev引用
            //tail节点不会有next引用
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

为当前线程创建Node节点之后,会先尝试一次CAS操作设置tail节点为当前线程,但是在多个线程同时竞争的情况下(这种情况会很少),接下来会在一个死循环当中,不断的尝试让当前线程成为tail节点,直到成功为止。

请求锁

当前线程进入等待线程队列之后,就会紧接调用acquireQueued(final Node node, int arg)或者doAcquireNanos(int arg, long nanosTimeout)

//arg在ReentrantLock中,一直都是1
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //死循环,一直到持有锁为止,中间会被阻塞
        for (;;) {
            //获取当前node的前节点prev
            final Node p = node.predecessor();
            //如果prev是头结点,并且请求锁成功
            //将prev节点剔除出队列,并将当前节点设置为head节点
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果prev节点不是头结点或者请求锁失败
            //移除掉队列中已经被取消的线程节点
            //或者将非SIGNAL状态的prev节点设置为SIGNAL状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                //会阻塞当前线程,之后检查线程是否中断
                //如果中断返回
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //如果死循环请求锁失败,
        if (failed)
            cancelAcquire(node);
    }
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            //省略与acquireQueued(final Node node, int arg)相同代码
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        //省略与acquireQueued(final Node node, int arg)相同代码
    }
}

能够请求锁的唯一条件,就是当前线程的pred引用指向head节点,根据tryAcquire(int arg)返回结果,判断请求锁是否成功。

成功

当前线程是等待线程队列中的第二个线程,并且tryAcquire(int arg)返回true时,就是当前线程持有锁了:

if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

将head节点设置为当前线程节点,然后清除之前head节点的所有引用指针。

而根据if条件,在设计时,考虑了当前线程已经是head节点的next,但仍然无法持有锁的情况。什么时候会发生这种情况呢?

在上面,FairSync是否一定是公平锁中说过,当用户调用ReentrantLock的tryLock()这个无参方法请求锁时,最后去调用Sync中实现的nonfairTryAcquire(int acquires),这时就存在即使满足了p == head这个条件,仍然无法获取到锁的情况。另一方面,成为head节点的线程肯定持有过锁。

失败

请求锁失败后,主要的流程在shouldParkAfterFailedAcquire(Node pred, Node node)方法中:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //这个检查实际上是在检查pred节点的状态是否是正常的
    //SIGNAL状态是需要被通知的状态
    if (ws == Node.SIGNAL)
        return true;
    //waitStatus只有唯一一个大于0的状态就是CANCELLED
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

在这个方法中看到,如果pred节点waitStatus是SIGNAL,就返回true,这会让线程在接下来让自己进入阻塞状态。其余情况下都是返回false,但是waitStatus是CANCELLED,会操作队列元素,移除掉从pred到pred之前最近一个正常节点的下一个节点这段节点链,其他状态时会让pred节点状态变为SIGNAL,可以看作激活操作。

异常

请求锁的整个过程当中会抛出异常的地方主要是node.predecessor()和线程被中断时,这些操作都会导致请求锁操作失败,接着就会执行cancelAcquire(Node node)方法:

private void cancelAcquire(Node node) {
    //省略部分代码
    //移除尾节点,并且将pred的next引用移除
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        //如果pred节点不是head节点,并且状态正常,那么从队列中移除node
        //但是如果pred已经是head节点,那么pred现在是持有锁的,那么需要唤醒node的next节点的线程做好请求锁的准备工作
        int ws; 
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

前面一部分代码很好理解,主要理解一下if流程,可以看作为三个分支:

1.当前节点是tail节点,并且成功的将tail.pred节点设置为新的tail节点成功;
2.确认node.pred节点不是head结点,并且状态不是CANCELLED;
3.node.pred是head节点或者node.pred节点已经取消请求锁操作;

第一个分支,更新tail节点,并将tail.next引用赋为null,因为流程最为简单,所以放到最开始。

第二个分支,就是单纯的将当前node从等待线程队列中移除。

第三个分支,唤醒node节点之后最近一个状态非CANCELLED节点的线程。

第二个分支的判断条件中,最后一个条件是pred.thread != null,node中的thread只有两种情况下会被赋为null,一个是晋升为head节点,一个是执行cancelAcquire(Node node)时。

第三个分支会执行unparkSuccessor(Node node)

private void unparkSuccessor(Node node) {
    //修改当前节点waitStatus为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //一般情况,都会直接唤醒下一个节点中的线程
    //但是某些情况下,当前节点是tail节点,又或者下一个节点的状态也是取消状态,
    //找一个与当前最近的一个等待中的节点唤醒
    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);
}

这里的代码就很清晰了,就是为了找到一个node之后,最近的一个非CANCELLED状态的节点,然后唤醒这个节点中的线程,尝试请求锁。

为什么这方法中没有操作队列,移除其中CANCELLED状态的检点呢?

如果这个被唤醒的线程,在被唤醒后,就会在死循环中,如果请求锁成功,它会成为head节点,这个时候,这个节点之前的所有元素都会被清除出队列。如果请求锁失败,那么会调用shouldParkAfterFailedAcquire(Node pred, Node node),在这个里面会清理队列中的截点。

释放锁

释放锁时代码很简单:

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(int arg)会一直返回false。

总结

结构设计

等待线程队列操作是CAS操作,代码考虑的各方面的场景较多,代码会比阻塞式更为复杂。

特点

1.公平锁与非公平锁
公平锁模式和非公平锁模式并非绝对的。
公平锁可以使用tryLock()方法,在特殊的时刻,无视等待线程队列中线程顺序直接获取锁。
非公平锁只是在比较特殊的时刻下,可以由某个正在执行的线程直接请求到锁,但是错过特殊的时刻,也需要按照队列顺序获得持有锁的机会。

2.可重入
已持有锁的线程可以重复请求同一个锁,如果重入后,释放次数少于请求次数,仍然会造成业务上的死锁,所以请求和释放锁时需要格外注意。

3.支持超时返回
支持线程在一段时间内无法获取锁时,直接返回失败,可以避免过多的阻塞线程占用资源。

ps: 了解更多ReentrantLock相关的知识,可以查看参考资料

参考资料

重入锁死
如何避免重入锁死
ReenTrantLock可重入锁(和synchronized的区别)总结
深入理解ReentrantLock

你可能感兴趣的:(ReentrantLock源码通读(一))