ReentrantLock中的NonfairSync加锁流程

NonfairSync

重入锁中的非公平锁,尝试获取锁的线程有可能会成功,如果不成功的话,则会进入AQS的队列中。

NonfairSync加锁流程

  1. ReentrantLock.lock函数中,会调用到NonfairSync.lock方法,首先会通过CAS方法,尝试将当前的AQS中的State字段改成从0改成1,如果修改成功的话,说明原来的状态是0,并没有线程占用锁,而且成功的获取了锁,只需要调用setExclusiveOwnerThread函将当前线程设置成持有锁的线程即可。否则,CAS操作失败之后,和普通锁一样,调用acquire(1)函数尝试获取锁。 /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
  2. 而在acquire(1)函数中,会判断tryAcquire(1)以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg),如果尝试获取失败并且添加队列成功的话,那么就会调用selfInterrupt函数中断线程执行,说明已经加入到了AQS的队列中。 在NonfairSync的tryAcquire中,会调用到nonfairTryAcquire函数。
    /**
     * Performs non-fair tryLock.  tryAcquire is
     * implemented in subclasses, but both need nonfair
     * try for trylock method.
     */
    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;
    }

在nonfairTryAcquire函数中,会尝试让当前线程去获取锁:

  1. 获取当前线程,以及AQS的状态
  2. 如果当前AQS的状态为0的话,那么说明当前的锁没有被任何线程获取,则尝试做一次CAS操作,将当前的状态设置成acquires,如果设置成功了的话,那么则将当前线程设置成锁持有的线程,并且返回true,表示获取成功。
  3. 如果当前的状态不为0的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,如果相同的话,则将当前的状态加上acquires重新将状态设置,并且返回true,这也就是重入锁的原因。
  4. 如果当前线程没有获取到锁的话,那么就会返回false,表示获取锁失败

而在addWaiter方法则会新建一个Node,然后将节点添加到队列中,让这个节点成为 tail。

 private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
  1. 根据Mode新建一个Node对象,而在上面传进来的Mode则是Node.EXCLUSIVE,然后得到尾节点tail,判断当前的尾节点是否为空,如果尾节点不为空的话,那么则将当前节点的prev设置成tail,也就是将自己作为尾节点添加
  2. 然后通过CAS操作,判断尾节点是否有修改过,如果当前的尾节点没有变化过,并且将node成功设置成尾节点的话,那么则将之前尾节点的next设置成当前的node,并且返回尾节点node
  3. 否则调用enq(node)将当前节点添加到队列尾部,并且返回node,而在enq方法中,则会判断头节点和尾节点是否初始化,如果没有初始化则会初始化,然后通过自旋的方式,将tail的next设置成node,并且将node的prev设置成tail,然后将node设置成tail。以达到将node设置成尾节点的目的

接着调用acquireQueued函数,传入尾节点。

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

在acquireQueued()中,当前线程会等待它在“CLH队列”中前面的所有线程执行并释放锁之后,才能获取锁并返回。如果“当前线程”在休眠等待过程中被中断过,则调用selfInterrupt()来自己产生一个中断。

  1. 进入这个方法后,可以看到会进入一个死循环,这个死循环中,只有当p == head && tryAcquire(arg)才会返回,这个条件代表的是,只有当当前节点的前驱是头节点,并且已经成功获取锁了,才会将当前的节点设置成头节点,并且前节点的next设置成空帮助GC回收,并且将failed标记成失败,并且返回当前线程是否被中断了。
  2. 如果当前节点的前驱不是头节点的话,那么则会判断当前线程在获取锁失败后, 是否需要阻塞,如果需要阻塞的话,就会调用parkAndCheckInterrupt方法进行当前线程的阻塞,并且在线程唤醒后,返回是否当前线程已经中断。

而在sholdParkAfterFailedAcquire函数中,会判断当前节点的前驱的状态,如果当前前驱的状态为Node.SIGNAL的话,那么说明,当前持有锁的线程正在阻塞,需要等它释放了锁之后才能获取,所以返回true,表示需要等待锁的释放,阻塞请求线程。否则,如果持有锁的线程的状态>0的话,说明前驱节点已经处于CANCEL状态,那么就会进入一个循环,直到找到一个状态小于0的(也就是SINGAL,CONDITION,PROPAGETE)状态的节点,然后把该节点的next设置成当前节点,中间的那些CANCEL节点就都被抛弃掉了,如果是PROPAGETE状态的话,那么说明需要一个信号,但是先不阻塞当前线程,调用者会继续尝试获取锁,于是就通过一个CAS操作,将前驱节点的waitStatus设置成Node.SIGNAL,并且告知当前线程不用阻塞。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏calvin

【nodejs】让nodejs像后端mvc框架(asp.net mvc)一orm篇【如EF般丝滑】typeorm介绍(8/8)

在使用nodejs开发过程中,刚好碰到需要做一个小工具,需要用到数据库存储功能。而我又比较懒,一个小功能不想搞一个nodejs项目,又搞一个后端项目。不如直接在...

3162
来自专栏Python小屋

Python回文判断代码优化与6个思考题

送个福利:清华大学出版社和新宝图书专营店联合推出正版特价图书《Python程序设计开发宝典》,原价69元,特价47.6元,详情:https://detail.t...

3386
来自专栏JavaEdge

探索 JUC 之美---可重入读写锁 ReentrantReadWriteLock可重入读写锁 ReentrantReadWriteLock实现AQS只有一个状态,那么如何表示 多个读锁 与 单个写锁

4385
来自专栏JackieZheng

探秘Tomcat——启动篇

tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container。具体请看下图: ? ...

4597
来自专栏Pythonista

牛掰的python与unix

  加载subprocess模块仅仅是将可以使用的代码文件加载进来。也可以创建自己的模块或文件,拱以后重复使用,这与加载subprocess模块的方法相同。IP...

1052
来自专栏破晓之歌

Django框架下admin.py的中文修改 原

#所以更改setttings.py 下 LANGUAGE_CODE = 'zh-Hans' 

1052
来自专栏hotqin888的专栏

engineercms利用pdf.js制作连续看图功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

1631
来自专栏后端之路

使用分页插件的后悔药(二)

背景 我们使用了pageHelper之后大部分的需求可以满足了 部分场景下不需要使用count等语句来做分页,只需要做sql查询 问题 小伙伴在使用了一段时间之...

2876
来自专栏dotnet & java

讲一下Asp.net core MVC2.1 里面的 ApiControllerAttribute

ASP.NET Core MVC 2.1 特意为构建 HTTP API 提供了一些小特性,今天主角就是 ApiControllerAttribute. (注:文...

1242
来自专栏游戏杂谈

bat与jscript开发工具时遇到的一些问题

之前使得bat调用luac进行编译时,会弹出一个“黑色的界面”,闪烁一下,感觉不太好。而脚本vbs或者jscript调用bat是可以利用Run方法,将其第二个参...

1152

扫码关注云+社区