AQS之ReentrantLock详解

news2024/11/17 23:53:56

非公平锁加锁过程

一般我们在使用ReentrantLock的时候,代码如下:

@Test
public void test(){
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    try{
        //编写业务逻辑
    }catch (Exception e){
        lock.unlock();
    }
}

当我们在用ReentrantLock独占锁的时候,如果不指定公平非公平,那默认是非公平的,如下:

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

当我们在外部调用lock()方法的时候会进入ReentrantLock内部的加锁lock方法,其中sync是ReentrantLock的内部类,sync直接继承了AbstractQueuedSynchronizer(AQS)。

public void lock() {
    sync.lock();
}

因为Sync内部的lock()方法是一个抽象方法,如下:

这里以非公平锁为例,代码调用lock会来到ReentrantLock 内部类NonfairSync的lock()方法,如下:

lock内部,一上来直接CAS操作AQS内部的一个state变量,从0到1如果修改成功,则获取锁。

否则else调用AQS内部的acquire方法:

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

acquire方法内部会调用tryAcquire方法,点击去会来到AQS内部的tryAcquire,这个方法没有在内部实现,是为了让子类去实现,模板方法模式的体现。

这里又会回到ReentrantLock 的内部类NonfairSync的tryAcquire方法:

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

接着调用nonfairTryAcquire方法:

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

该方法大致逻辑:

1. 获取AQS内部的state变量,如果等于0说明没有线程获取锁,继续CAS尝试获取锁,如果成功返回true。(如果是公平锁,这里需要判断队列里是否有阻塞的线程等着,入队前)
2. 如果不等于0,则判断当前线程current是否等于已经获取锁的线程,如果等于则+1,返回true,这里就是重入锁的逻辑。
3. 如果CAS即没有获取锁,同时也不是重入锁,则返回false。

到这里,返回false,会回到最初调用tryAcquire方法的地方:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //如果acquireQueued返回true,说明阻塞线程在等待的过程中被中断,因为线程被唤醒没有获取到锁需要继续阻塞,导致parkAndCheckInterrupt内部调用了Thread.interrupted()方法给中断标志清除了,所以这里需要把中断标志重新恢复过来
        selfInterrupt();
}

这里是竞争共享资源失败需要入队等待唤醒的逻辑,首先调用addWaiter(Node.EXCLUSIVE)方法将当前线程封装成一个Node结点。

addWaiter方法的代码如下:(详细解释见注释)

将当前线程封装成Node结点同时入队(尾插法--->注意入队的3个步骤

//添加阻塞线程
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;
    //将pred指针指向队列尾结点
    if (pred != null) {
        //如果不等于null,说明队列有等待线程
        node.prev = pred;
        //修正当前node结点的前驱指针,指向队列的尾结点
        //CAS尝试将当前结点置为尾结点
        if (compareAndSetTail(pred, node)) {
            //修正队列尾结点的后继指针,指向当前结点
            pred.next = node;
            //当前结点入队成功则返回
            return node;
        }
    }
    //代码走到这里说明,队列里没有阻塞的线程
    enq(node);
    return node;
}
//将结点入队
private Node enq(final Node node) {
    //这里是CAS+自旋机制,保证当前结点一定要入队成功,因为存在并发的情况
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //这里是给队列进行初始化,构造头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //这里是重点,线程入队步骤,如果这几步调换是会存在安全问题的,这3步顺序不能改变
            node.prev = t;
            //1.将当前node结点的前驱指针,指向队列的尾结点
            if (compareAndSetTail(t, node)) {
                //2. CAS尝试将当前结点置为尾结点
                //3. 修正队列尾结点的后继指针,指向当前结点
                t.next = node;
                return t;
            }
        }
    }
}


//尾插法进行入队
//为什么释放锁的时候是从尾部往前遍历,不能从前往后,关键就在于尾插法的三个步骤
//    <------     这里是自己操作自己的指针没有并发冲突
//      cas       只有一个能设置成功
//    ------>
//顺序不能改变,否则会有线程安全问题(null指针)
//注意:解锁的时候是从尾部往前遍历的

addWaiter方法结束回到acquireQueued方法处,详细解释看注释

  1. 这里大致逻辑是阻塞线程入队后获取前驱结点

  1. 如果前驱结点是头结点 出于性能考虑,会再次尝试获取锁

bb. 抢到锁了,将当前结点设置成头结点

  1. 判断shouldParkAfterFailedAcquire是不是需要park,并且梳理一下指针的指向关系

  1. 需要park则调用parkAndCheckInterrupt方法

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);
                //释放原来的头结点,断点指针,让gc来进行回收 
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //到这里说明 当前结点的前驱结点不是头结点 或者 前驱结点是头结点但是没有拿到锁
            //说明前驱结点是SIGNAL状态,那就调用park阻塞
            //判断是不是需要park,并且梳理一下指针的指向关系
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //记录阻塞线程在等待获取锁的过程中有没有被中断
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


//设置头结点
private void setHead(Node node) {
    //这里因为只有一个结点会进来,所以不需要CAS
    head = node;
    node.thread = null;
    node.prev = null;
}


//这里需要先了解下AQS内部的结点状态
/** waitStatus value to indicate thread has cancelled */
//取消线程结点
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
//表示后面的线程需要被唤醒
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
//该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
//表示下一次共享式同步状态获取将会被无条件地传播下去
static final int PROPAGATE = -3;


//判断前驱结点状态,找到一个前驱结点状态是SIGNAL的,如果不是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) {
        //前驱结点状态是 CANCELLED 状态需要被取消
        //意思就是没有必要在 CANCELLED 结点后面等着了,前驱结点不会进行通知当前结点,那就往前面找,直到不大于0,也就是不是取消 CANCELLED 状态的
        /*
         * 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;
}

来到parkAndCheckInterrupt方法:

private final boolean parkAndCheckInterrupt() {
    //将当前线程进行阻塞,去睡觉,让出CPU执行权
    LockSupport.park(this);
    //返回中断标志位,并重置为false
    //如果有其他线程调用interrupt方法将其唤醒,当前线程如果获取不到锁,仍然需要阻塞,所以这里需要通过interrupted方法清除中断标志,如果不清除线程会一直在这里自旋,耗费CPU资源
    return Thread.interrupted();
}



//延伸:这里为啥要用park,而不是wait、sleep
//wait
//1.调用wait需要释放锁,而释放锁的前提是当前线程得持有锁,没拿到锁就不能释放    wait需要和notify配合使用
//2.而且notify不能指定唤醒的线程
//sleep
//1.sleep不能确定要睡多久


// LockSupport.park(), 可通过两种方式被唤醒。
// 1.LockSupport.unpark()
// 2.interrupt()

public void interrupt() {}    // 给线程打一个中断标志
public boolean isInterrupted() {} // 检测下线程是否被中断,不清除中断标志
public static boolean interrupted() {}    //也是检测下线程是否被中断,但清除中断标志  

这里如果不清楚线程中断的含义和使用,可以参考:Java线程中断

来张图吧:

非公平锁解锁过程

当我们在外部调用lock.unlock()方法的时候,代码如下:

 public void unlock() {
        sync.release(1);
 }

可以看到方法里会通过sync内部类调用AQS内部的release方法:

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方法内部又会调用子类的tryRelease方法(ReentrantLock):

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

protected final void setState(int newState) {
    state = newState;
}

tryRelease方法大致的执行过程:

  1. 获取AQS内部的state变量,并将其-1

  1. 为了代码的健壮性判断当前解锁线程是不是获取锁的线程(防御性编程

  1. 如果等于0,将exclusiveOwnerThread属性置为null

  1. 修改state变量值(注意这里没有CAS,因为独占锁只有一个线程能获取到锁)

执行完tryRelease方法,会判断如果队列的头结点不等于null并且waitStatus不等于0,就到了unparkSuccessor方法:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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);
}

该方法大致过程如下:

  1. 恢复头结点head的waitStatus 状态 compareAndSetWaitStatus(node, ws, 0)

  1. 获取头结点的下一个结点Node s = node.next 如果等于null 或者 waitStatus大于0

  1. 从tail结点开始往前遍历,找到前边的waitStatus 小于等于0的结点

  1. 找到了对应的node结点调用LockSupport类的unpark方法唤醒线程的阻塞。

来张图吧:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164002.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

current并发包

并发包 current并发包、在JDK1.5之前Java并没有提供线程安全的一些工具类去操作多线程&#xff0c;需要开发人员自行编写实现线程安全&#xff0c;但仍然无法完全避免低性能、死锁、资源管理等问题。在JDK1.5时新增了java.util.current并发包&#xff0c;其中提供了许多供我们…

【自学Python】Python截取字符串

Python截取字符串 Python截取字符串教程 在 Python 中&#xff0c;我们需要截取 字符串&#xff0c;不需要使用特定的 函数&#xff0c;只需要使用下标索引加上切片的形式&#xff0c;就可以实现字符串的截取。 Python字符 Python 中没有单个字符的概念&#xff0c;单个字符…

uni-app 微信小程序通过Vue3 Hooks 实现动态填充页面剩余高度

应用场景 在uni-app开发微信小程序等项目时&#xff0c;经常会遇到这样的页面布局需求&#xff1a;上半部分高度固定&#xff0c;下半部分自动占满剩余高度&#xff0c;如下图所示应用场景&#xff1a;上半部分为固定高度或内容填充高度的内容区域下半部分为scroll-view滑动区…

河北稳控科技振弦采集模块配置工具VMTool的常见功能

河北稳控科技振弦采集模块配置工具VMTool的常见功能 一、实时数据读取 当 VMTool 与模块为连接状态时&#xff08; 4.3.1 模块的连接与断开&#xff09;&#xff0c; 勾选实时数据区的【 自动读取】 复选框&#xff0c; VMTool 开始自动向模块发送实时数据读取指令&#xff0c…

如何用 Java 来构建一个简单的速率限制器?

速率限制 现实世界中的用户是残暴的&#xff0c;并且没耐心&#xff0c;充满着各种不确定性。在高并发系统中&#xff0c;可能会出现服务器被虚假请求轰炸的情况&#xff0c;因此您可能希望控制这种情况。 一些实际使用情形可能如下所示&#xff1a; API配额管理-作为提供者…

28.函数指针变量作为函数的参数,容易混淆的指针概念,特殊指针,main函数传参

1.函数指针变量作为函数的参数 #include<stdio.h> int add(int x, int y) {return x y; } int sub(int x, int y) {return x - y; } int mux(int x, int y) {return x * y; } int dive(int x, int y) {return x / y; } int process(int(*p)(int, int),int x,int y) {in…

Redis基础命令操作三之集合类型SET

SET集合 特点&#xff1a;集合中存储的元素是惟一的。 命令举例说明SADD sadd [key] [value1 value2 value3]key对应的集合中添加元素SMEMBERSsmembers [key]获取key对应的集合的所有元素SISMEMBERsismember [key] [value]判断value是否在key对应的集合中存在SCARDscard [key…

excel日期函数:EDATE与DATE到底谁更胜一筹

平时的工作中&#xff0c;经常会遇到计算职工转正日期、合同到期日、职工退休日期以及产品有效期截止日等等与日期有关系的问题。这些问题看似复杂&#xff0c;实际上只需要一个很简单的函数就能搞定&#xff0c;这个函数就是EDATE。今天分享EDATE函数的几个应用实例&#xff0…

【Linux】Linux 权限和权限管理

文章目录Linux权限的概念Linux权限管理文件访问者的分类&#xff08;人&#xff09;文件类型和访问权限&#xff08;事物属性)目录权限默认权限粘滞位关于权限的总结Linux权限的概念 权限是用来限制人的&#xff0c;权限人事物属性 权限存在的意义是便于系统安全管理的 Linux下…

Python处理第一类切比雪夫多项式

第一类切比雪夫多项式简介 Chebyshev多项式是一种非常重要的正交多项式&#xff0c;在逼近理论中有重要应用&#xff0c;第一类切比雪夫多项式的根可用于多项式插值&#xff0c;对弥补龙格现象有很大的帮助。其表达形式为 Tncos⁡(narccos⁡x)T_n\cos(n\arccos x) Tn​cos(nar…

vite+vue3+Ts搭建基础项目框架

随着前端技术的更新&#xff0c;程序员们的技术栈也要不断跟上&#xff0c;本来想躺平&#xff0c;不料却被推着走。 上个月开发团队新来一个项目需求&#xff0c;要求开发技术栈vue2更换成vue3&#xff0c;毫无准备的小编一脸懵&#xff0c;嗯&#xff1f;怎么说 换就换了&am…

读写分离有哪些坑?

在上一篇文章中,我和你介绍了一主多从的结构以及切换流程。今天我们就继续聊聊一主多从架构的应用场景:读写分离,以及怎么处理主备延迟导致的读写分离问题。 我们在上一篇文章中提到的一主多从的结构,其实就是读写分离的基本结构了。这里,我再把这张图贴过来,方便你理解…

【Linux】Linux环境下如何实现自动化编译——make/makefile入门

文章目录 前言 一、make/Makefile 的使用 1.示例 2.编写 Makefile 文件 2.1生成 2.2清理 二、make 如何知道生成的可执行文件是否最新 前言 在Linux 环境下编写好C语言代码之后&#xff0c;我们需要使用编译工具gcc 将其翻译为可执行文件。可是&#xff0c;如果对代码进…

Apache Spark 机器学习 特征抽取 4-1

特征数据集是用于在机器学习中进行训练&#xff0c;有关特征的算法的分类如下所示&#xff1a; 抽取&#xff08;Extraction&#xff09;&#xff0c;从原始数据集中提取出对应的特征集 转换&#xff08;Transformation&#xff09;&#xff0c;缩放特征、转换特征以及修改特征…

线程池(关于变量捕获、线程数、针对ThreadPoolExecutor的构造方法参数的解释、自实现线程池)

目录&#xff1a;一、前言二、关于变量捕获三、针对ThreadPoolExecutor的构造方法参数的解释四、自实现线程池一、前言相比较于进程&#xff0c;创建线程 / 销毁线程 的开销是相对较小的&#xff0c;但是太过频繁的创建线程 / 销毁线程&#xff0c;其开销也很大。这时候我们就需…

C 语言编译链接

前言 一个 C 程序究竟是怎么变成可执行程序的&#xff0c;这其间发生了什么&#xff1f;本文将带你简要了解 C 程序编译过程&#xff0c;文章为 《程序员的自我修养—链接、装载与库》的读书笔记&#xff0c;更为详细的过程可以阅读原书。 比如下面一个经典的 C 程序&#xf…

百度飞浆在pycharm中的使用(含官网安装和cuda)

uieGitHub 安装cuda 1 获取版本 我的是 CUDA Toolkit 11.7.1 (August 2022), Versioned Online Documentation 为了防止后期版本不对应&#xff0c;我这里小心谨慎安装了August对应的月份。 C:\Users\89735>nvidia-smi Mon Dec 19 21:31:28 2022 ------------------------…

一眼万年,这3款顶级神软,内存满了也绝不卸载

免费软件都不好用&#xff1f;不&#xff01;下面3款良心软件&#xff0c;颠覆你的认知&#xff0c;功能强大到离谱&#xff0c;值得收藏往后有需要直接使用。 1、桌面运维助手 这是一款堪称神器的国产电脑优化工具&#xff0c;集硬件管理、系统管理、辅助工具于一体&#xff0…

Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

Effective C条款39&#xff1a;明智而审慎地使用private继承&#xff08;Use private inheritance judiciously&#xff09;条款39&#xff1a;明智而审慎地使用private继承1、private 继承2、在private继承和复合之间做出正确选择3、使用private继承比组合更加合理的例子4、牢…

wsl安装CUDA

NVCC 昨天已经安装好了gpu版的pytorch&#xff0c;对于一般的代码应该就可以运行了。但有些代码中需要用到cuda算子&#xff0c;需要配置nvcc环境。对于这个我也没能搞太清楚&#xff0c;网上的说法不一&#xff0c;我使用conda安装pytorch时也安装了cudatoolkit&#xff0c;按…