图解ReentrantLock可重入锁

图解ReentrantLock可重入锁

目录

  1. 介绍
  2. 结构图
  3. 非公平锁实现
    1. 获取锁
    2. 排队等待
  4. 公平锁实现

介绍

从ReentrantLock这个类的注释我们可以得知,它的用法跟synchronized关键字的用法差不多,甚至功能相对来说更加丰富。

跟synchronized一样,ReentrantLock对于程序员来说也是一把,用于负责一个时间内只有一个线程能够获得它并用来做一些特定的操作。单从这样看的话,两者的功能看起来也没有太多区别。

但是实际上,两者底层的原理却不太一样,比如如果一个线程第一次没有办法获取到锁,在synchronized实现的程序中,线程会被阻塞,一直等到锁被释放再去获取锁,而在ReentrantLock中,线程会有个轮询状态,去不断的尝试获取锁,避免因为阻塞引发的上下文切换所导致的资源浪费(当然,轮询一定次数锁还没有释放的时候,它也会乖乖的把线程挂起等待被唤醒,毕竟一直让cpu空转也是一个不太好的选择)。

在确定了synchronized与ReentrantLock确实存在不同的时候,学习后者的必要性的显而易见了。

结构图

在了解ReentrantLock的一些原理的之前,需要先对它的结构有一定的了解,这样更方便我们理解后续的一些原理。


请添加图片描述

首先,ReentrantLock里面有一个Sycn内部类,它继承了AQS这个抽象队列同步器,而在AQS中几个重要的属性,可以把它抽象为一个计数器和一个队列,计数器state的用法相当于记录这个锁有没有被占用,被获取了多少次。而队列就相当于这个锁有那些线程在等待它。

再换种比方,如果把一个ReentrantLock对象比作一个包子铺,那么占有这个锁的线程就相当于一个顾客,在跟店里的服务员(服务员只有一个)说要买多少个包子,state就等于记录顾客找服务员要了多少个包子,而队列就等于在店外面排队的人。


/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    @ReservedStackAccess
    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;
    }
}

紧接着Sync之后,也有两个内部类,同时也继承了Sync这个抽象类,它们两个代表了ReentrantLock的两种不同的实现方式——公平和非公平(这个我们后续通过画图来描述)。

可以看到两种实现都重写了一个叫tryAcquire的方法,翻译过来就是尝试获取嘛,这个时候就会突出与synchronized的不同之处,ReentrantLock即使没获取到锁也没关系,直接返回false就行了,让上层去做其他操作,如果上层非要拿到锁,那么就去排队,不拿也可以,那么就去做别的事。

非公平锁

获取锁

上面提到了非公平锁,那么我们从一个买包子的故事来展开吧。

请添加图片描述

假设今天早上包子店开门,然后门口就来了一位顾客,这个时候顾客就会跟服务员说,要一个包子,如果state==0,说明服务员现在有空,那么接收下来这个订单,让state+1。然后给顾客返回true。这个时候顾客占有了这个包子店,也就相当于一个线程获取到了这把锁。

下面就是对应获取锁的源码

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    if (c == 0) {
        // 锁没有被占用
        if (compareAndSetState(0, acquires)) {
            // 在state上记录要几个包子
            // 标注当前锁被哪个线程所占有
            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;
}

那么下面这个else if分支又是什么意思呢?请看图!

请添加图片描述

线程占有这个锁的这个时间段内,线程可以多次去获取这把锁,方法内只需要将state递增1即可。(state也有一个阈值,当它大于Integer的最大值出现溢出的时候,会直接抛出一个Error)

排队等待

上面提了两种情况,都是基于在只有一个线程之下的操作,那么假设现在包子店门口又来了一个顾客呢?
请添加图片描述

这个时候服务员经过一系列的判断,确认没办法为这个顾客提供服务,所以只能返回一个false。换到线程这边就是锁已经被占用了,线程没办法获取到锁。

既然没办法获取到锁,返回的false会去到哪里呢?那我们来看一下ReentrantLock的调用链
请添加图片描述

根据这个调用链(正常线程获取锁都是通过lock方法写入),我们可以得知是否获取到锁的结果,最终都返回到了这个acquire方法里面,那么我们可以拿源码过来研究一下。

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

从代码中可以得知,执行了tryAcquire方法没有拿到锁之后,那么就会执行*acquireQueued(addWaiter(Node.EXCLUSIVE), arg)*这个方法(如果获取到锁,那就结束调用返回到上一个方法)。那这个方法就是我们接着要分析的排队。

回顾一下一开始的结构图,不难发现在我们获取锁的时候就用到了state这个成员变量,但是还有一个head和tail组成的队列还没有使用,而head和tail对应的类就是一个Node类,这个类是AQS中的一个内部类,抽象来看的话,我们可以把它理解为一个有前后指针的节点,对应的value就是排队的Thread线程。废话少说,上图!
请添加图片描述

这个就可以理解为AQS内部维护的一个链表,链表中存储的就是没能够获取到锁的线程。

现在我们开始来分析一下*acquireQueued(addWaiter(Node.EXCLUSIVE), arg)*这里的代码

首先是addWaiter

private Node addWaiter(Node mode) {
    // 创建了一个新的Node,通过Thread.currentThread()获取到当前执行的线程,将其包装进Node中
    Node node = new Node(mode);

    for (;;) {
        // 
        // 当前状态下队列的尾部
        Node oldTail = tail;
        if (oldTail != null) {
            // 将之前的尾部节点设置为当前node节点的前置节点
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                // cas原子操作,更新队列的尾节点,如果更新成功则执行以下操作,如果失败则进入下一次循环继续进行cas操作
                // 将原来的尾节点的next指针指向当前的node
                oldTail.next = node;
                return node;
            }
        } else {
            // tail为空说明队列还不存在,初始化队列
            // 初始化队列只做了几件事情,new一个Node作为head,thread的值为空(对应了上图),将head也赋值给tail,形成一个新的队列
            initializeSyncQueue();
        }
    }
}

这个时候可能会有一些疑惑,cas之后的赋值操作不会引发多线程问题吗,其实并不会,假设有两个线程在获取原先队列的尾节点,并且对队列的尾节点进行修改,这个时候cas保证了只有一个线程是可以执行成功的,当原子操作修改了队列的tail之后,另一个线程的cas会失败,然后执行下一次循环,那么下一次循环获取到的tail就是前一个线程的node了。

做完addWaiter这个操作之后,我们再来看acquireQueued这个方法

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            // 获取node节点的前置节点
            final Node p = node.predecessor();
            // 如果前置节点p是头节点,
            // 那么说明node节点处在队列的第一位
            if (p == head && tryAcquire(arg)) {
                // 再次调用tryAcquire方法获取锁,
                // 获取到了说明前一个占用锁的线程在这个线程生成Node期间释放了锁
                // 重新设置head节点,将node的thread字段清空为null
                setHead(node);
                // 原来的前置节点取消掉next的引用,帮助垃圾回收
                p.next = null; // help GC
                return interrupted;
            }
            // 能够执行到这里,说明这个节点不是队列中第一个排队的节点
            // 或者没办法抢占到锁
            if (shouldParkAfterFailedAcquire(p, node))
                /**
                	shouldParkAfterFailedAcquire这个方法用于去检查和更新那些没能拿到锁的节点
                	这块我们等会分析
                */
                // 当上面的方法返回true的时候,说明这个节点需要被阻塞,不能继续轮询了
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

分析完acquireQueued,我们再去看一下shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前置节点的等待状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 如果前置节点的信号为这个,说明node节点需要被阻塞
        /*
         * 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.
         */
        // 大于0说明前置节点已经被取消,那么就循环将node节点前被取消的节点都给抛弃,
        // 直到它指向一个不被取消的前置节点
        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.
         */
        // 将前置节点的状态设置SIGNAL,但是node节点不会立刻挂起,而是再次尝试去获取一次锁,
        // 当它再次进来这个方法之后,这个方法会返回true(只要prev没有被取消)
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

那样总结下来,我们可以得到一个这样的场景。

请添加图片描述

这个方法的操作,就避免了线程一直在轮询去尝试获取锁,从而导致cpu空转。

来到这里,我们了解了如何获取锁以及排队等待,而释放锁的逻辑其实就是对state做减法操作,当state减到0的时候,就等于线程把锁释放掉了,这个时候也会对等待队列进行唤醒操作,让其继续进行轮询尝试获取锁。

公平锁

上面讲到的是非公平锁,那么它与公平锁最大的区别,稍微抽象的总结其实就是会不会去插队。

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方法,并不会像非公平锁那样,当state==0的时候,不会直接尝试去获取锁,而是先去判断是否存在等待队列。如果等待队列存在的话,那么就会返回false,进行排队操作。那么整个流程其实就是,当占有锁的线程释放锁以后,会唤醒队列中的线程,而在唤醒前突然有一个线程尝试获取锁,那么它也会进去排队,而不是获取锁。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值