ReentranLock超详细讲解

news2024/9/30 15:19:58

目录

  • ReentranLock
    • AQS底层
      • AQS的变量、常量与内部类
        • Node内部类
    • FairSync:公平锁
      • lock方法
      • AbstractQueuedSynchronizer的acquire方法
      • tryAcquire方法
        • getState方法
        • setExclusiveOwnerThread方法
        • 总结tryAcquire方法
      • 返回AbstractQueuedSynchronized的acquire方法
      • acquireQueued方法
        • setHead方法
        • shouldParkAfterFailedAcquire方法
        • parkAndCheckInterupt方法
        • cancelAcquire方法

ReentranLock

在ReentrantLock中,调用lock方法获取锁,调用unlock方法来释放锁,当然要同一个ReentrantLock才能锁住,具体的用法回看Java复习的锁

ReentrantLock拥有公平锁和非公平锁,我们可以从构造方法中看出,无参构造出来的锁是非公平锁
在这里插入图片描述
ReentrantLock的实现依赖于Java的同步框架AbstractQueuedSynchronizer(简称为AQS)

AQS是使用一个整形的volatile变量来维护同步状态的,名字为state

所以,我们必须先看看AQS的底层源码

AQS底层

AQS的变量、常量与内部类

在这里插入图片描述
这里我们只先了解head、tail与一个内部类Node,不过在上面,我们也可以看到一个重要的state变量,就是前面提到的锁实现

因为这三个就构成了AQS的底层,一个双向链表形成的队列,该队列就有用来存储线程的

Node内部类

在这里插入图片描述
可以看到里面不仅有prev、next还有要存储的线程thread,当然里面还有一些变量是表示线程状态的,比如CANCELLED、CONDITION、PROPAGATE和SIGNAL,而且要注意,这个内部类是一个final类型,不可以被继承,但结点里面的prev、next和thread都是非final,可以被修改的!
在这里插入图片描述
先认识到这,接下来我们回去看看ReentrantLock

接下来,我们看看ReentrantLock的结构在这里插入图片描述
可以看到,ReentrantLock里面有3个内部类

分别为

FairSync:公平锁
NonfairSync:非公平锁
Sync:锁

FairSync:公平锁

在这里插入图片描述

可以看到FairSync继承了Sync,而且自己只有一个序列化Id属性(当然还包含Sync的一切属性和方法)

lock方法

不过最重要的,我们还是要看他的lock方法
在这里插入图片描述
可以看到,他调用AbstractQueuedSynchronizer里面的acquire方法,而且给了一个参数为1,因为Sync是继承了AbstractQueuedSynchronizer,AbstractQueuedSynchronizer是一个抽象类,所以FairSync是拥有AbstractQueuedSynchronizer的所有属性和方法

AbstractQueuedSynchronizer的acquire方法

接下来,我们看看AbstratQueuedSynchronizer的acquire方法
在这里插入图片描述

tryAcquire方法

在这里插入图片描述
可以看到这个方法是AbstractQueuedSynchronizer里面的,而且并没有实现,在下面这些类里面都有实现
在这里插入图片描述
因为我们看的是FairSync,所以FairSync调用的tryAcquire方法肯定是FairSync里面的

下面是他的底层实现在这里插入图片描述
可以从注释中知道,这里的tryAcquire是公平版本的,符合FairSync的本质,接下来分析源代码(公平锁传进来的acquire为1)

protected final boolean tryAcquire(int acquires) {
	//获取当前执行该方法的线程
    final Thread current = Thread.currentThread();
	//获取锁的状态,前面提到过,AQS是依靠一个volatile变量来实现的
	//而且该变量称为state,在tryAcquire这里就实现了
    int c = getState();
	//等于0,代表锁还没有获取
    if (c == 0) {
        //调用了hasQueuedPredecessors与compareAndSetState方法
//hasQueuedPredecessors,来判断是否需要排队,因为是公平锁
        //compareAndSetState方法其实就是CAS去获取锁
        //而且可以看到,如果CAS成功,那么state是改成acquire,也就是1
        //所以1代表了锁正在被占用中
        //所以ReentrantLock的底层是CAS
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //如果不需要等待,且拿到了锁
            //将锁的拥有者设为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//如果锁已经被占用
	//而锁的拥有者再一次去进行获得锁
	//即拥有锁的线程又去获得锁
    else if (current == getExclusiveOwnerThread()) {
        //计算当前状态+acquires
        int nextc = c + acquires;
        //如果小于0 抛出异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //锁的状态变成
        setState(nextc);
        //继续放行,所以允许锁的拥有者再次获得锁
        //但仅仅修改了底层state状态
        return true;
    }
//如果锁被占用,而且请求获得的线程不是锁的占用者
//返回false
    return false;
}

getState方法

在这里插入图片描述
就是返回了底层的state变量,也就是AQS的state

hasQueuedPredecessors方法
这个方法是用来判断线程是否需要排队的(返回false不需要排队),因为是公平锁,所以这个判断是必须的,否则就不公平了,因为没有判断排队操作,那么只要一个线程释放了锁,那么后来的线程也是可以抢到的(透露一下:非公平锁的实现就是仅仅少了这个判断而已)
在这里插入图片描述
可以看到,这个方法是AQS里面的,tail和head属性前面已经提到过,是底层队列的头尾结点

关键在于return语句

他主要有两个判断

头结点是否等于尾结点
头结点是否尾空,或者头结点的线程是否为当前线程
关于这个弹出,必须要先清楚,底层队列里面的结点是怎么添加的

下面就是AQS的插入方法addWaiter
在这里插入图片描述
可以看到,如果尾结点不为空,也就代表不是第一次插入,那么直接就使用尾插法,如果是第一次插入,只是调用enq方法,所以enq方法是针对第一次插入使用的,这两个方法都是AQS的
在这里插入图片描述
可与看到里面是一个死循环,如果是第一次插入,让创建一个新结点为头结点(相当于一个哨兵),然后让新尾结点一样,这一点可以看成是初始化头尾结点,接下来下一轮循环,此时尾结点已经不为null,接下来就让新结点指向尾结点,让新结点又称为了尾结点,再让旧的尾结点指向新的尾结点

但这里有一个疑问,如果并不是第一次入队,为什么最后还要执行多一次enq方法呢?

我们可以看到,如果并不是第一次入队,因为前面的addWaiter,使用了一次CAS,那如果CAS失败了怎么办?即中途有线程把尾结点改了怎么办?

此时就需要重复的操作,不断的执行CAS直到成功为止,所以就有了无论哪次插入都要执行enq方法,因为enq方法里面就是一个死循环的CAS执行,直到CAS执行成功,才会结束

总结一下添加结点

所以头结点只是一个哨兵,是不存储线程的,而插入的方法则是尾插法

其实头结点也不完全是哨兵,下面我们看线程自旋时,会发现其实头结点是当前轮到执行的线程,这里先埋下个伏笔

知道了插入方法后,我们回过头来看hasQueuedPredecessors的返回值,也就是如何判断是否需要排队
在这里插入图片描述
步骤如下

判断头结点是否等于尾结点,只有在初始化的时候,头结点才会等于尾结点,或者队列为空,头尾结点都为null,所以,如果头结点等于尾结点(直接返回false),就代表了队列为空,所以就不需要进行排队
通过第一个判断就已经知道头尾结点之间是有其他结点存在的,但由于前面的enq方法是先处理头结点的,然后除了两个CAS操作之外,其他都不是原子性的,也就是基本上整个enq方法都不是原子性的,所以当正在插入第一个线程的时候,会出现一个h.next == null的问题(这时已经代表有一个线程正在插入了,但还没完成插入,所以当前线程需要排队),所以当h.next == null时就代表要排队
最后一个判断就是判断头结点的下一个线程是不是当前线程(也就是下一个是不是轮到你,不需要排队,因为插入方法是尾插法,所以弹出方法要从头弹出),经过前面两次判断,可以得知队列中有人在排队,并且此时并不存在队列第一次插入结点的Null问题,那么最后就需要判断下一个是否轮到当前线程

setExclusiveOwnerThread方法

在这里插入图片描述
当前面判断无人排队,或者下一个就是轮到当前线程,那么首先要做的是调用setExclusiveOwnerThread方法

这个方法唯一执行的是:将exclusiveOwnerThread改成为自己,表明这个锁被这个线程获取了

总结tryAcquire方法

首先获取当线程

获取锁的当前状态,0代表无人占用

如果无人占用,判断当前线程是否需要进行排队

如果队列为空,不需要排队

如果队列不为空,但下一个是轮到自己,也不需要为空

这里要注意,第一次插入的null问题

如果当前线程不需要进行排队,下一步就是执行CAS来获得锁(此时锁state的状态要由0变为了1),CAS保证了同时争夺锁的并发安全性,如果此时CAS失败,就要重头来(本质上是CAS修改state从0变为1)

如果抢到了锁,那就将锁的拥有者改成自己
返回true给上一层

如果锁已经被占用了,也就是state不为0,分为两种情况

其他线程占有这把锁,所以后续操作直接返回false

当前线程占有这把锁,又再次进行加锁,相当于加了多层锁,又要去修改锁的状态state,一般来说会加1(源码上是加上acquire,而acquire传进来是为1),代表这个锁进入了需要被释放多次,才可以被其他线程获取,然后返回True给上一层

如果是第二种情况,是不需要使用CAS来保证线程安全性的,因为上一层已经有一层锁在保护这个线程的安全性

整个判断是否排队的tryAcquire方法已经结束了

接下来就是回到上一层的acquire方法

返回AbstractQueuedSynchronized的acquire方法

在这里插入图片描述
可以看到,如果需要排队,就会执行acquireQueued方法,接下来在执行addWaiter

addWaiter方法已经看过了,其实就是一个结点进入底层队列而已,返回的是插入的结点,相当于是尾结点,也就是入队操作,不过这里离要注意的是他的参数是一个Node.EXCLUSIVE,我们接下来来看一下他究竟是什么
在这里插入图片描述
其实本质上这只是一个状态表示,表示入队这个结点是进入等待状态

acquireQueued方法

在这里插入图片描述
接下来我们看看这个方法干了什么

前面已经判断了是否需要等待,如果需要等待,就会先执行插入队列的方法,然后执行acquireQueued,既然进入了队列了,那么此时就应该是park此时的线程,也就是停止此时的线程,但其实停止并不是休眠或者阻塞,在ReentrantLock中,是使用自旋的方式来进行的

其实,这个方式其实就是ReentranLock底层的自旋操作

接下来分析整一段源码

	final boolean acquireQueued(final Node node, int arg) {	 
	 //定义一个failed变量
     //该变量用来判断当前线程拿到锁是否失败
        boolean failed = true;
        try {
            //定义一个interrupted变量
            //这个变量用来判断当前线程是否还在排队(还在自旋)
            boolean interrupted = false;
            //死循环
            //这个死循环就是底层的CAS自旋
            for (;;) {
                //获取当前线程结点的前一个结点
                final Node p = node.predecessor();
                //如果前一个线程是头结点,然后当前线程就会去看是否需要排队
                //前面已经提到过,头结点的线程要么是一个哨兵,要么就是当前拥有锁的线程
                //如果前一个线程是头结点,那就代表当前线程就要时刻注意前一个线程是否结束
                //所以要死循环不断地去看需不需要排队
                //如果前一个结点并不是头结点,因为是公平锁,那就不需要在意,继续等待
                if (p == head && tryAcquire(arg)) {
                    //进入到这里,就代表前一个线程已经结束了
                    //并且当前线程抢到锁,正在执行
                    //先将自己改成头结点,代表自己正在操作,让下一个线程注意
                    //并且这个setHead不是普通让自己成为头结点
                    //他会将自己里面的线程设为
                    //因为这是要满足一个原则:线程队列里面不允许存在拥有锁的线程
                    setHead(node);
                    //断开原来头结点与自己的连接
                    p.next = null; // help GC
                    //failed变量设为false
                    //代表线程拿到锁
                    failed = false;
                    //返回interrupted
                    //该线程结束自旋,开始执行
                    return interrupted;
                }
                //当争夺锁失败时
                //调用shouldParkAfterFailedAcquire方法
                //这个方法是检查和更新未能获取锁的线程状态,判断是否需要进行阻塞
                //如果需要阻塞就会调用parkAndCheckInterrupt
                //也就是让这个线程进行阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //获取不到锁,failed依然为true
                    //获取不到锁,还需要继续自旋,执行下一轮循环
                    interrupted = true;
            }
        } finally {
			//当轮到线程执行时,需要结束自旋,开始执行动作
            //即failed会变为false
            //但首先要做的是,
            if (failed)
                cancelAcquire(node);
        }
    }

前面的自旋循环很简单,但是一直自旋下去并不是好事,因为自旋也是会消耗CPU的,假如有一个线程迟迟不释放锁,就白白消耗了很多的CPU,所以,自旋是要限制次数的,而限制次数就存在于当获得锁失败时可能会调用的两个方法

setHead方法

在这里插入图片描述
可以看到,setHead方法将头结点里面的线程设为了空,因为要满足获取锁的线程不存在于队列中,代表获取锁的线程已经不需要再进行排队了,而且不需要被叫醒

shouldParkAfterFailedAcquire方法

前面已经提到过这个线程是用来判断获取锁失败的线程是否需要park,即是否需要阻塞(因为CAS太多次了)

我们先来看看源码

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       //获取前一个线程的waitStatus状态
        int ws = pred.waitStatus;
       //如果前一个线程状态为SIGNAL
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            //返回true,即当前线程需要阻塞、休眠
            return true;
       //如果前一个线程的状态大于0
        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.
             */
            //如果是0或者其他状态
            //那么前一个线程还是可以继续自旋的,不过将状态改成了SIGNAL
            //当前线程下一轮抢锁如果还是失败,那他的上一个线程就会因为变成了SIGNAL
            //而被取消线程,转而变成阻塞了
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
       //不需要park掉线程
        return false;
    }

那么我们来看看这个SIGNAL状态是什么
在这里插入图片描述
这个值为-1,线程的waitStatus为-1就代表了这个线程不可以自旋了,要阻塞、休眠了,所以也可以waitStatus为-1也可以表示线程休眠

这里就会有一个问题,为什么要让队列中的上一个线程去决定当前线程是否需要睡眠(为什么要去修改上一个线程的waitStatus为SIGNAL),即挂起当前线程??

这是因为前面的都等的都要休眠了,后面的更加需要进行休眠(前面的都等的睡着了,后面的也可以睡着,毕竟公平锁要前面的执行完才会到后面的),但其实这个waitStatus只是一个标志性睡眠,不代表线程真的被挂起了,

那么又有一个问题,自旋几次才会进入休眠状态??

答案是自旋两次,第一次自旋,抢不到锁,将上一个线程视为睡眠,也就是将其waitStatus状态改为SIGNAL,第二次自旋,发生前面的线程已经是睡眠了,那么就挂起

parkAndCheckInterupt方法

这个方法其实就是让线程挂起,只有判断了线程需要挂起才会去执行
这个方法的作用是,让线程进入等待状态,等待状态可以被两种动作唤醒

其他线程尝试对该线程进行interrupt
被其他线程进行unpark
如果是被其他线程进行interrupt唤醒的,就代表有其他线程想终止当前线程,那么Thread.interrupted就会返回true,如果不是,是第二种方式的,就会返回false
在这里插入图片描述
可以直到其调用了LockSupport.part方法

这个方法其实就是禁用线程的,也就是将线程挂起
在这里插入图片描述

cancelAcquire方法

可以看到,从acquireQueued方法里面,进行了一系列try语句里面的流程之后,最后还有一个finally语句块,这个语句块是调用了cancelAcquire的,而且要在failed变量为true的时候才会执行

现在问题来了,failed变量在自旋死循环中,只会变成false,而不会变成true,而且线程挂起之后更加不会走到这行代码,那么什么时候会调用这个acquireQueued方法呢?

我们可以知道,这里要执行acquireQueued方法的唯一途径就是try语句块里面抛出了异常,中断了try里面的代码,而且try里面的代码并没有将failed修改为true

那么这个抛出异常的地方在那里呢?

回顾一路走过来,抛出异常处只有一个地方
在这里插入图片描述
也就是当调用tryAcquire去看是否需要排队时,当拥有锁的线程又再一起去申请获得锁,可能会抛出异常,不过这个异常抛出也是有前提的,也就是锁的状态要小于0?

我也不太清楚什么情况会让锁的状态小于0

接下来看看这个cancelAcquire方法做了什么

这个方法实际上就是让这个线程取消获取锁

源码如下

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            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
        }
    }

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

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

相关文章

「AI知多少」第二期推荐《AIGC:智能创作时代》

一、书名 《AIGC&#xff1a;智能创作时代》 二、简介 在人工智能发展的漫长历程中&#xff0c;如何让机器学会创作一直被视为难以逾越的天堑&#xff0c;“创造力”也因此被视为人类与机器最本质的区别之一。然而&#xff0c;人类的创造力也终将赋予机器创造力&#xff0c;…

YTM32的增强型定时器eTMR外设模块详解

文章目录 eTMR外设简介eTMR工作机制系统框图引脚与信号计数器与时钟源输出比较模式PWM模式通道配对通道对的互补输出&#xff08;Complementary Mode&#xff09;双缓冲输出PWM&#xff08;Double Switch&#xff09;错误检测机制&#xff08;Fault Detection&#xff09; 输入…

搭建confluence

confluent是一款由JAVA编写用于企业知识库管理协同软件&#xff0c;多用于构建企业内部WIKI&#xff0c;支持多人协作&#xff0c;共享信息等。 当前系统环境Centos7.9&#xff0c;内存至少2G以上&#xff0c;数据库采用MySQL5.7&#xff0c;本机电脑系统Windows10 安装前准备…

uboot图形化配置界面添加自定义菜单

一. 简介 图形化配置工具的主要工作就是在 .config 下面生成前缀为“ CONFIG_ ”的变量&#xff0c;这些变量一般都要值&#xff0c;为 y &#xff0c; m 或 n &#xff0c;在 uboot 源码里面会根据这些变量来决定编译哪个文件。 本文我 们就来学习一下如何添加自己的自…

十大排序算法(C语言)

参考文献 https://zhuanlan.zhihu.com/p/449501682 https://blog.csdn.net/mwj327720862/article/details/80498455?ops_request_misc%257B%2522request%255Fid%2522%253A%2522169837129516800222848165%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&…

汽车行驶性能的主观评价方法(1)-底盘校准方法

底盘校准的目的是&#xff0c;从行驶性能和行驶舒适性两个方面进行协调&#xff0c;从而优化行驶动力学特性。为了达到这一目标&#xff0c;工程人员早在设计阶段&#xff0c;就对大多数对行驶动力性有重要意义的部件提出了要求。这些要求不仅与底盘的组件有关&#xff0c;还必…

PDE 中的先验估计是什么意思?

见 知乎 https://www.zhihu.com/question/52567966

现在学编程还有出路吗?程序员的出路在哪里?

程序员的出路有很多&#xff0c;以下是一些常见的职业发展方向&#xff1a; 技术专家 。程序员可以专注于技术领域&#xff0c;成为某个技术领域的专家&#xff0c;并从事技术咨询、培训、研发等方面的工作。创业者 。程序员可以结合自身技术背景&#xff0c;创办自己的公司&a…

【App 抓包提示网络异常怎么破?】

背景 当你测试App的时候,想要通过Fiddler/Charles等工具抓包看下https请求的数据情况,发现大部分的App都提示网络异常/无数据等等信息。以“贝壳找房”为例: 455 x 705 Fiddler中看到的请求是这样的: 619 x 215 你可能开始找证书的问题:是不是Fiddler/Charles的证书没有…

哪些场景需要额外注意线程安全问题

今天我们主要学习哪些场景需要额外注意线程安全问题&#xff0c;在这里总结了四种场景。 访问共享变量或资源 第一种场景是访问共享变量或共享资源的时候&#xff0c;典型的场景有访问共享对象的属性&#xff0c;访问 static 静态变量&#xff0c;访问共享的缓存&#xff0c;…

python内置函数sorted()

列表对象提供了sort()方法支持原地排序,而内置函数sorted()返回新列表,并不对原列表进行任何修改sorted()可以对列表,元组,字典,range对象等进行排序列表的sort()方法和内置函数sorted()都支持key参数实现复杂排序要求 例1 person [{name: zhangsan, age: 37},{name: lisi, …

CSRF跨域请求伪造

1.SSRF服务端请求伪造&#xff08;外网访问内网&#xff09; SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下&#xff0c;SSRF是要目标网站的内部系统。&#xff08;因为他是从内部系统访问的&#xf…

常见的云测试策略及重要性

随着云计算技术的快速发展&#xff0c;云服务已经成为了现代应用程序开发和部署的核心组成部分。然而&#xff0c;随之而来的是对云系统性能和质量的不断追求&#xff0c;这使得云测试变得至关重要。本文将探讨云测试的概念、重要性以及一些常见的云测试策略和工具。 一、云测试…

适用于嵌入式arm的ffmpeg编解码

在嵌入式arm应用开发中&#xff0c;经常会遇到需要处理视频的情况&#xff0c;这时候就需要强大的开源工具ffmpeg出马了。 这里可以下载到各个版本的ffmpeg。 ffmpeg各版本https://www.videohelp.com/software/ffmpeg/old-versions 现在ffmpeg更新较频繁&#xff0c;如…

第二证券:监管效能不断提升 并购重组步入“全面注册制时代”

跟着本年2月全面注册制革新相关原则的正式发布&#xff0c;上市公司并购重组亦全面步入“注册制时代”。本钱商场根底性原则的适应性、包容性明显前进&#xff0c;为推进上市公司做强主业、前进质量提供助力&#xff0c;也让并购重组商场的“老旋律”在全面注册制的布景下弹出“…

java实现周易64卦并返回对应的卦象(含百度百科链接)

《易经》是中华民族传统思想文化中自然哲学与人文实践的理论根源&#xff0c;是古代汉民族思想、智慧的结晶&#xff0c;被誉为“大道之源”&#xff0c;是古代帝王之学&#xff0c;政治家、军事家、商家的必修之术。 《易经》含盖万有&#xff0c;纲纪群伦&#xff0c;是中华…

seacms_CNVD-2020-22721_v10.1漏洞分析与复现

seacms 远程命令执行漏洞复现 文章目录 seacms 远程命令执行漏洞复现一、基本信息二、组件简介三、漏洞详情漏洞介绍影响范围危害 四、防御1. 漏洞存在性检测2. 修复建议3. 规避措施4. 漏洞利用检测 五、漏洞复现1. 复现环境2. 漏洞复现 一、基本信息 titlecontentnote漏洞编号…

pytest-yaml 测试平台-3.创建执行任务定时执行用例

前言 当项目用例编写完成后&#xff0c;需设置执行策略&#xff0c;可以用到定时任务设置每天几点执行。或者间隔几个小时执行一次。 创建定时任务 创建任务 勾选需要执行的项目以及运行环境 触发器可以支持2种方式&#xff1a;interval 间隔多久触发和 cron 表达式定时执行…

为什么进行压力测试? 有哪些方法?

在信息技术飞速发展的今天&#xff0c;软件系统的性能已经成为了用户满意度的决定性因素之一。而要确保一个系统在实际使用中能够稳定可靠地运行&#xff0c;压力测试就显得尤为关键。本文将深入探讨什么是压力测试&#xff0c;为什么它是如此重要&#xff0c;以及一些常见的压…

【Unity数据交互】JSON开山篇

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…