今天用图解的方式从源码角度给大家说一下 ReentrantLock 加锁解锁的全过程。系好安全带,发车了。

简单使用

在聊它的源码之前,我们先来做个简单的使用说明。当我在 IDEA 中创建了一个简单的 Demo 之后,它会给出以下提示:

公平锁和非公平锁_公平锁和非公平锁有哪些_公平锁和非公平锁区别

提示信息

在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。

java.concurrent.LockShouldWithTryFinallyRule.rule.desc

还举了两个例子,正确的案例如下:

Lock lock = new XxxLock();// ...lock.lock();try {    doSomething();    doOthers();} finally {    lock.unlock();}

错误的案例如下:


Lock lock = new XxxLock();// ...try {    // 如果在此抛出异常,会直接执行 finally 块的代码    doSomething();    // 不管锁是否成功,finally 块都会执行    lock.lock();    doOthers();
} finally { lock.unlock();}

AQS

上边的案例中加锁调用的是 lock() 方法,解锁用的是 unlock() 方法,而通过查看源码发现它们都是调用的内部静态抽象类 Sync 的相关方法。

abstract static class Sync extends AbstractQueuedSynchronizer

Sync是通过继承 AbstractQueuedSynchronizer 来实现的。没错,AbstractQueuedSynchronizer 就是 AQS 的全称。AQS 内部维护着一个 FIFO 的双向队列(CLH),ReentrantLock 也是基于它来实现的,先来张图感受下。

公平锁和非公平锁有哪些_公平锁和非公平锁_公平锁和非公平锁区别

Node 属性


//此处是 Node 的部分属性static final class Node {    //排他锁标识    static final Node EXCLUSIVE = null;    //如果带有这个标识,证明是失效了    static final int CANCELLED = 1;    //具有这个标识,说明后继节点需要被唤醒    static final int SIGNAL = -1;    // Node对象存储标识的地方    volatile int waitStatus;    //指向上一个节点    volatile Node prev;    //指向下一个节点    volatile Node next;    //当前Node绑定的线程    volatile Thread thread;
//返回前驱节点即上一个节点,如果前驱节点为空,抛出异常 final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; }}


对于里边的 waitStatus 属性,我们需要做个解释:(非常重要)

AQS 属性


// 头结点private transient volatile Node head;// 尾结点private transient volatile Node tail;//0->1 拿到锁,大于0 说明当前已经有线程占用了锁资源private volatile int state;


今天我们先简单了解下 AQS 的构造,以帮助大家更好的理解 ReentrantLock。至于深层次的东西先不做展开。

加锁

对 AQS 的结构有了基本了解之后,我们正式进入主题——加锁。从源码中可以看出锁被分为公平锁和非公平锁


/** * 公平锁代码 */final void lock() {    acquire(1);}
/** * 非公平锁代码 */final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);}


初看代码发现非公平锁似乎包含公平锁的逻辑,所以我们就从“非公平锁”开始。

非公平锁

final void lock() {    //通过 CAS 的方式尝试将 state 从0改为1,    //如果返回 true,代表修改成功,获得锁资源;    //如果返回false,代表修改失败,未获取锁资源    if (compareAndSetState(0, 1))        // 将属性exclusiveOwnerThread设置为当前线程,该属性是AQS的父类提供的        setExclusiveOwnerThread(Thread.currentThread());    else        acquire(1);}

compareAndSetState():底层调用的是 unsafe的compareAndSwapInt,该方法是原子操作;

假设有两个线程(t1、t2)在竞争锁资源,线程 1 获取锁资源之后,执行 setExclusiveOwnerThread 操作,设置属性值为当前线程 t1。

公平锁和非公平锁_公平锁和非公平锁有哪些_公平锁和非公平锁区别

此时,当 t2 想要获取锁资源,调用 lock() 方法之后,执行 compareAndSetState(0, 1) 返回 false,会走 else 执行 acquire() 方法。

方法查看

public final void accquire(int arg) {    // tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false    if (!tryAcquire(arg) &&        // 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        // 线程中断        selfInterrupt();}

accquire() 中涉及的方法比较多,我们将进行拆解,一个一个来分析,顺序:tryAcquire() -> addWaiter() -> acquireQueued()。

查看 tryAcquire() 方法

// AQS中protected boolean tryAcquire(int arg) {    // AQS 是基类,具体实现在自己的类中实现,我们去查看“非公平锁”中的实现    throw new UnsupportedOperationException();}
// ReentrantLock 中protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);}
final boolean nonfairTryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); //获取AQS 的 state int c = getState(); // 如果 state 为0,代表尝试再次获取锁资源 if (c == 0) { // 步骤同上:通过 CAS 的方式尝试将 state 从0改为1, //如果返回 true,代表修改成功,获得锁资源; //如果返回false,代表修改失败,未获取锁资源 if (compareAndSetState(0, acquires)) { //设置属性为当前线程 setExclusiveOwnerThread(current); return true; } } //当前占有锁资源的线程是否是当前线程,如果是则证明是可重入操作 else if (current == getExclusiveOwnerThread()) { //将 state + 1 int nextc = c + acquires; //为什么会小于 0 呢?因为最大值 + 1 后会将符号位的0改为1 //会变成负数(可参考Integer.MAX_VALUE + 1) if (nextc < 0) // overflow //加1后小于0,超出锁可重入的最大值,抛异常 throw new Error("Maximum lock count exceeded"); //设置 state 状态 setState(nextc); return true; } return false;}


因为线程 1 已经获取到了锁,此时 state 为 1,所以不走 nonfairTryAcquire() 的 if。

又因为当前是线程 2,不是占有当前锁的线程 1,所以也不会走 else if,即 tryAcquire() 方法返回 false。

查看 addWaiter() 方法

走到本方法中,代表获取锁资源失败。addWaiter() 将没有获取到锁资源的线程甩到队列的尾部。

private Node addWaiter(Node mode) {    //创建 Node 类,并且设置 thread 为当前线程,设置为排它锁    Node node = new Node(Thread.currentThread(), mode);    // 获取 AQS 中队列的尾部节点    Node pred = tail;    // 如果 tail == null,说明是空队列,    // 不为 null,说明现在队列中有数据,    if (pred != null) {        // 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点        node.prev = pred;        // CAS 将 tail 节点设置为当前节点        if (compareAndSetTail(pred, node)) {            // 将之前尾节点的 next 设置为当前节点            pred.next = node;            // 返回当前节点            return node;        }    }    enq(node);    return node;}


当 tail 不为空,即队列中有数据时,我们来图解一下 pred!=null 代码块中的代码。初始化状态如下,pred 指向尾节点,node 指向新的节点。

公平锁和非公平锁区别_公平锁和非公平锁_公平锁和非公平锁有哪些

node.prev = pred;

将 node 的前驱节点设置为 pred 指向的节点。

公平锁和非公平锁_公平锁和非公平锁区别_公平锁和非公平锁有哪些

compareAndSetTail(pred, node);

通过 CAS 的方式尝试将当前节点 node 设置为尾结点,此处我们假设设置成功,则 FIFO 队列的 tail 指向 node 节点。

公平锁和非公平锁有哪些_公平锁和非公平锁区别_公平锁和非公平锁

pred.next = node; 

将 pred 节点的后继节点设置为 node 节点,此时 node 节点成功进入 FIFO 队列尾部。

公平锁和非公平锁_公平锁和非公平锁有哪些_公平锁和非公平锁区别

而当 pred 为空,即队列中没有节点或将 node 节点设置为尾结点失败时,会走 enq() 方法。我们列举的例子就符合 pred 为空的情况,就让我们以例子为基础继续分析吧。

//现在没人排队,我是第一个 || 前边CAS失败也会进入这个位置重新往队列尾巴去塞private Node enq(final Node node) {    //死循环    for (;;) {        //重新获取tail节点        Node t = tail;        // 没人排队,队列为空        if (t == null) {            // 初始化一个 Node 为 head,而这个head 没有意义            if (compareAndSetHead(new Node()))                // 将头尾都指向了这个初始化的Node,第一次循环结束                tail = head;        } else {            // 有人排队,往队列尾巴塞            node.prev = t;            // CAS 将 tail 节点设置为当前节点            if (compareAndSetTail(t, node)) {                //将之前尾节点的 next 设置为当前节点                t.next = node;                return t;            }        }    }}


进入死循环,首先会走 if 方法的逻辑。通过 CAS 的方式尝试将一个新节点设置为 head 节点,然后将 tail 也指向新节点。

可以看出队列中的头节点只是个初始化的节点,没有任何意义。

公平锁和非公平锁有哪些_公平锁和非公平锁区别_公平锁和非公平锁

继续走死循环中的代码,此时 t 不为 null,所以会走 else 方法。将 node 的前驱节点指向 t,通过 CAS 方式将当前节点 node 设置为尾结点,然后将 t 的后继节点指向 node。至此,线程 2 的节点就被成功塞入 FIFO 队列尾部。

公平锁和非公平锁有哪些_公平锁和非公平锁_公平锁和非公平锁区别

查看 acquireQueued() 方法

将已经在队列中的 node 尝试去获取锁否则挂起。

final boolean acquireQueued(final Node node, int arg) {    // 获取锁资源的标识,失败为 true,成功为 false    boolean failed = true;    try {        // 线程中断的标识,中断为 true,不中断为 false        boolean interrupted = false;        for (;;) {            // 获取当前节点的上一个节点            final Node p = node.predecessor();            // p为头节点,尝试获取锁操作            if (p == head && tryAcquire(arg)) {                setHead(node);                p.next = null;                // 将获取锁失败标识置为false                failed = false;                // 获取到锁资源,不会被中断                return interrupted;            }            // p 不是 head 或者 没拿到锁资源,            if (shouldParkAfterFailedAcquire(p, node) &&                // 基于 Unsafe 的 park方法,挂起线程                parkAndCheckInterrupt())                interrupted = true;        }    } finally {        if (failed)            cancelAcquire(node);    }}

这里又出现了一次死循环。

首先,获取当前节点的前驱节点 p。如果 p 是头节点(头节点没有意义),说明 node 是 head 后的第一个节点。此时,当前获取锁资源的线程1可能会释放锁,所以线程 2 可以再次尝试获取锁。

假设获取成功,证明拿到锁资源了。将 node 节点设置为 head 节点,并将 node 节点的 pre 和 thread 设置为 null。因为拿到锁资源了,node 节点就不需要排队了。

将头节点 p 的 next 置为 null,此时 p 节点就不在队列中存在了,可以帮助 GC 回收(可达性分析)。failed 设置为 false,表明获取锁成功;interrupted 为 false,则线程不会中断。

公平锁和非公平锁_公平锁和非公平锁有哪些_公平锁和非公平锁区别

如果 p 不是 head 节点或者没有拿到锁资源,会执行下面代码,因为我们的线程 1 没有释放锁资源,所以线程 2 获取锁失败,会继续往下执行。

//该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    //获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。    int ws = pred.waitStatus;    // 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回true    if (ws == Node.SIGNAL)        return true;    // 如果上一个节点的状态大于0,说明已经失效了    if (ws > 0) {        do {            // 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给            // pred            node.prev = pred = pred.prev;        } while (pred.waitStatus > 0); // 一直找到小于等于0的        // 将重新标识好的最近的有效节点的 next 指向当前节点        pred.next = node;    } else {        // 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    }    return false;}

只有节点的状态为 -1,才会唤醒后一个节点,如果节点状态未设置,默认为 0。

图解一下 ws>0 的过程,因为 ws>0 的节点为失效节点,所以 do…while 中会重复向前查找前驱节点,直到找到第一个 ws

限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: lzxmw777

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注