AQS-ReentrantLock

news2024/11/30 0:46:36

一、AQS

image.png
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具,也是 Lock 用来实现线程同步的核心组件。

1.AQS 的两种功能

独占和共享。

  • 独占锁:每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
  • 共享锁 ,允许多个线程同时获取锁 ,并发访问共享资源 ,比如ReentrantReadWriteLock

2.AQS的实现

AQS.png
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。
每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS 队列中去; 当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

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;

    // 存储在Condition队列中的后继节点
    Node nextWaiter;

  	// 返回前驱节点,如果前驱节点为null,抛出NPE
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    // 将线程构造成一个Node,添加到等待队列
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
   }

    // 在Condition队列中使用
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

3.AQS添加线程

添加节点.png

    1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
    1. 通过 CAS 将 tail 重新指向新的尾部节点

4.AQS释放锁

释放锁.png
head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点:

    1. 修改 head 节点指向下一个获得锁的节点
    1. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可。

二、CAS

1.CAS 的实现原理

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

通过 cas 乐观锁的方式来做比较并替换,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false。这个操作是原子的,不会出现线程安全问题。

2.state属性

private volatile int state;
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样, 对于重入
锁的实现来说,表示一个同步状态。它有两个含义的表示

  • 当 state=0 时,表示无锁状态
  • 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候, state 会递增,比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁

3.Unsafe类

Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、
Hadoop、 Kafka 等;

Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、 CAS、线程同步、内存屏障

而 CAS 就是 Unsafe 类中提供的一个原子操作:
public final native boolean compareAndSwapInt(Object obj, long stateOffset, int expect, int update);

  • obj:需要改变的对象
  • stateOffset:偏移量(即之前求出来的 headOffset 的值)
  • expect:期待的值
  • update:更新后的值

整个方法的作用是如果当前时刻的值等于预期值 expect 相等,则更新为新的期望值 update,如果更新成功,则返回 true,否则返回false;

stateOffset:
一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置
所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量

compareAndSwapInt
unsafe.cpp 文件中compareAndSwarpInt 的实现:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset,jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //将 Java 对象解析成 JVM 的 oop(普通对象指针) 
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p 和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比较并替换, x 表示需要更新的值, addr 表示 state在内存中的地址, e 表示预期值
UNSAFE_END

三、ReentrantLock

ReentrantLock时序图:

ReentrantLock NonfairSync Sync AbstractQueuedSynchronizer lock() lock() acquire() tryAcquire() nonfairTryAcquire() true/false addWaiter() ReentrantLock NonfairSync Sync AbstractQueuedSynchronizer

1.lock()

public void lock() {
	// sync分为了公平和非公平
    sync.lock();
}

sync是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。
AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒, 但它并不具备业务功能, 所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。

Sync 有两个具体的实现类:

  • NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
  • FailSync: 表示所有线程严格按照 FIFO 来获取锁

NonfairSync#lock():

final void lock() {
    // 通过CAS的方式尝试将state从0修改为1,如果返回true,代表修改成功,如果修改失败,返回false
    if (compareAndSetState(0, 1))
        // 将一个属性设置为当前线程,这个属性是AQS的父类提供的
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
  1. 非公平锁和公平锁最大的区别在于,非公平锁中抢占锁的逻辑是,不管有没有线程排队,直接cas去抢占
  2. CAS 成功,就表示成功获得了锁
  3. CAS 失败,调用 acquire(1)走锁竞争逻辑

2.AQS#acquire()

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时 acquire(1)操作

public final void acquire(int arg) {
	// tryAcquire再次尝试获取锁资源,如果尝试成功,返回true
    if (!tryAcquire(arg) &&
		// 获取锁资源失败后,需要将当前线程封装成一个Node,追加到AQS的队列中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		// 线程中断
        selfInterrupt();
}
  • 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  • 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
  • acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁

3.NonfairSync#tryAcquire()

方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false。
它是重写 AQS 类中的 tryAcquire 方法。

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,如果成功,设置ExclusiveOwnerThread属性为当前线程
        if (compareAndSetState(0, acquires)) {
            // 保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	// 当前占有锁资源的线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
		// 将state + 1
        int nextc = c + acquires;
		// 如果加1后,小于0,超所锁可重入的最大值,抛出Error
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
		// 没问题,就重新对state进行复制
        setState(nextc);
		// 锁重入成功
        return true;
    }
    return false;
}

4.AQS#addWaiter()

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能

  • 1.将当前线程封装成 Node
  • 2.当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
  • 3.如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
// 说明前面获取锁资源失败,放到队列中等待
private Node addWaiter(Node mode) {
	// 创建Node类,并且设置thread为当前线程,设置为排它锁
    Node node = new Node(Thread.currentThread(), mode);
  	// 获取AQS中队列的尾部节点,默认是 null
    Node pred = tail;
	// 如果tail != null,说明队列中存在节点
    if (pred != null) {
		// 把当前线程的 Node 的 prev 指向 tail
        node.prev = pred;
		// 通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
        if (compareAndSetTail(pred, node)) {
			// 设置成功以后,把原 tail 节点的 next指向当前 node
            pred.next = node;
            return node;
        }
    }
	// tail=null,把 node 添加到同步队列
    enq(node);
    return node;
}

// enq :通过自旋操作把当前节点加入到队列中
// 队列没有节点,我是第一个, 如果前面CAS失败,也会进到这个位置重新往队尾进入。
private Node enq(final Node node) {
	// 死循环
    for (;;) {
		// 重新获取当前的tail节点为t
        Node t = tail;
        if (t == null) { 
			// 队列没有节点, 我是第一个,没头没尾,都是空
            if (compareAndSetHead(new Node()))	// 初始化一个Node作为head,而这个head没有意义。
				// 将头尾都指向了这个初始化的Node
                tail = head;
        } else {
			// 有节点,往队尾入
			// 当前节点的上一个指向tail
            node.prev = t;
			// 基于CAS的方式,将tail节点设置为当前节点
            if (compareAndSetTail(t, node)) {   
				// 将之前的为节点的next,设置为当前节点
                t.next = node;
                return t;
            }
        }
    }
}

5.AQS#acquireQueued()

通过 addWaiter 方法把线程添加到链表后, 会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁:

  • 1.获取当前节点的 prev 节点
  • 2.如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
  • 3.抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
  • 4.如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
  • 5.通过 cancelAcquire 取消获得锁的操作
// 已经将node加入到了双向队列中,然后执行当前方法
final boolean acquireQueued(final Node node, int arg) {
	// 标识
    boolean failed = true;
    try {
		// 标识
        boolean interrupted = false;
        for (;;) {
			// 获取当前节点的上一个节点p
            final Node p = node.predecessor();
			// 如果p是头,说明有资格去争抢锁,尝试获取锁资源(state从0-1,锁重入操作),成功返回true,失败返回false
            if (p == head && tryAcquire(arg)) {
				// 获取锁成功,设置head节点为当前节点,将thread,prev设置为null,因为拿到锁资源了 ;
                setHead(node);
                p.next = null;   // 把原 head 节点从链表中移除,帮助GC回收
                failed = false;  // 将标识修改为false
                return interrupted;  // 返回interrupted 
            }
			// 保证上一个节点是-1,才会返回true,才会将线程阻塞,等待唤醒获取锁资源
            if (shouldParkAfterFailedAcquire(p, node) &&
				// 基于Unsafe类的park方法,挂起线程
                parkAndCheckInterrupt();   // 针对fail属性,这里是唯一可能出现异常的地方,JVM内部出现问题时,可以这么理解,fianlly代码块中的内容,执行的几率约等于0
                interrupted = true; // 返回当前线程在等待过程中有没有中断过
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire
如果 ThreadA 的锁还没有释放的情况下, ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法
Node 有 5 中状态

  • CANCELLED(1) :在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状态后的结点将不会再变化
  • SIGNAL(-1) :只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
  • CONDITION(-2)
  • PROPAGATE(-3):享模式下, PROPAGATE 状态的线程处于可运行状态
  • 默认状态(0)
    通过 Node 的状态来判断, ThreadA 竞争锁失败以后是否应该被挂起。
  1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false.
    返回 false 时,也就是不需要挂起,返回 true,则需要调用parkAndCheckInterrupt
    挂起当前线程
// node是当前节点,pred是上一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取上一个节点的状态
    int ws = pred.waitStatus;
	// 如果上一个节点状态为SIGNAL,意味着只需要等待其他前置节点的线程被释放
    if (ws == Node.SIGNAL)
        return true; // 返回 true,意味着可以直接放心的挂起了
	// ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点
    if (ws > 0) {
        do {
			// 将当前节点的prev指针指向了上一个的上一个
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);  // 一直找到小于等于0的,从双向列表中移除 CANCELLED 的节点
		// 将重新标识好的最近的有效节点的next
        pred.next = node;
    } else {
		// 小于等于0,不等于-1,将上一个有效节点状态修改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

使用 LockSupport.park 挂起当前线程变成 WATING 状态
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。 如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。

selfInterrupt:

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的。

cancelAcquire

// cancelAcquire方法
private void cancelAcquire(Node node) {
	// 如果当前节点为null,结束,健壮性判断
    if (node == null)
        return;
	// node不为null的前提下执行

	// 将当前node的线程置位null  , 竞争锁资源跟我没有关系了,
    node.thread = null;

	// 获取当前节点的前驱节点
    Node pred = node.prev;
	// 前驱节点的状态 > 0
    while (pred.waitStatus > 0)
		// 找到前驱中最近的非失效节点
        node.prev = pred = pred.prev;

	// 将第一个不是失效节点的后继节点声明出来
    Node predNext = pred.next;

	// 将当前节点置位失效节点。给别的Node看的。
    node.waitStatus = Node.CANCELLED;

	// 如果当前节点是尾节点,将尾节点设置为最近的有效节点(如果当前节点为尾节点的操作)
    if (node == tail && compareAndSetTail(node, pred)) {
		// 用CAS方式将尾节点的next设置null
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
		// 中间节点操作
		// 如果上一个节点不是头节点
        if (pred != head &&
			获取上一届点状态,是不是有效
            ((ws = pred.waitStatus) == Node.SIGNAL ||  // pred需要唤醒后继节点的
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);  // 尝试将pred的前驱节点的next指向当前节点的next(必须是有效的next节点)
        } else {
			// 头结点,唤醒后继节点
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

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

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

相关文章

Git 分支操作

1&#xff1a;什么是分支几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离 开来进行重大的Bug修改、开发新的功能&#xff0c;以免影响开发主线。 几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作…

2023“Java基础-中级-高级”面试集结,已奉上我的膝盖

Java基础&#xff08;对象线程字符接口变量异常方法&#xff09; 面向对象和面向过程的区别&#xff1f; Java 语言有哪些特点&#xff1f; 关于 JVM JDK 和 JRE 最详细通俗的解答 Oracle JDK 和 OpenJDK 的对比 Java 和 C的区别&#xff1f; 什么是 Java 程序的主类&…

GLOG如何控制输出的小数点位数

1 问题 在小白的蹩脚翻译演绎型博文《GLOG从入门到入门》中&#xff0c;有位热心读者提问说&#xff1a;在保存日志时&#xff0c;浮点型变量的小数位数如何设置&#xff1f; 首先感谢这位“嘻嘻哈哈的地球人”赏光阅读了小白这不太通顺的博客文章&#xff0c;并提出了一个很…

红旗语音助手HMI设计流程之调研篇

红旗智能语音助手是基于红旗4.0智能化平台打造的场景设计研究成果。本篇文章&#xff0c;将会以红旗语音助手为例&#xff0c;带领小伙伴们了解一下HMI设计中的调研工作。在项目中&#xff0c;我们需要要通过多模态的调研手段&#xff0c;去分辨用户的哪些需求是真需求&#xf…

【C++】string类的基本使用

层楼终究误少年&#xff0c;自由早晚乱余生。你我山前没相见&#xff0c;山后别相逢… 文章目录一、编码&#xff08;ascll、unicode字符集、常用的utf-8编码规则、GBK&#xff09;1.详谈各种编码规则2.汉字在不同的编码规则中所占字节数二、string类的基本使用1.string类的本质…

Hive---Hive语法(一)

Hive语法&#xff08;一&#xff09; 文章目录Hive语法&#xff08;一&#xff09;Hive数据类型基本数据类型&#xff08;与SQL类似&#xff09;集合数据类型Hive数据结构数据库操作创建库使用库删除库表操作创建表指定分隔符默认分隔符&#xff08;可省略 row format&#xff…

逆向工具之 unidbg 执行 so

1、unidbg 入门 unidbg 是一款基于 unicorn 和 dynarmic 的逆向工具&#xff0c; 可以直接调用 Android 和 IOS 的 so 文件&#xff0c;无论是黑盒调用 so 层算法&#xff0c;还是白盒 trace 输出 so 层寄存器值变化都是一把利器&#xff5e; 尤其是动态 trace 方面堪比 ida tr…

零基础机器学习做游戏辅助第十四课--原神自动钓鱼(四)yolov5目标检测

一、yolo介绍 目标检测有两种实现,一种是one-stage,另一种是two-stage,它们的区别如名称所体现的,two-stage有一个region proposal过程,可以理解为网络会先生成目标候选区域,然后把所有的区域放进分类器分类,而one-stage会先把图片分割成一个个的image patch,然后每个im…

关于SqlServer高并发死锁现象的分析排查

问题描述 通过定期对生产环境SqlServer日志的梳理&#xff0c;发现经常会出现类似事务与另一个进程被死锁在资源上&#xff0c;并且已被选作死锁牺牲品&#xff0c;请重新运行该事务的异常&#xff0c;简单分析一下原因&#xff1a;在高并发场境下&#xff0c;多个事务同时对某…

Ubuntu 使用Nohup 部署/启动/关闭程序

目录 一、什么是nohup&#xff1f; 二、nohup能做什么&#xff1f; 三、nohup如何使用&#xff1f; 四、怎么查看/关闭使用nohup运行的程序&#xff1f; 命令 实例 一、什么是nohup&#xff1f; nohup 命令运行由 Command参数和任何相关的 Arg参数指定的命令&#xff0c…

【微信小程序】--WXML WXSS JS 逻辑交互介绍(四)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#…

Kotlin 34. recyclerView 案例:显示列表

Kotlin 案例1. recyclerView&#xff1a;显示列表 这里&#xff0c;我们将通过几个案例来介绍如何使用recyclerView。RecyclerView 是 ListView 的高级版本。 当我们有很长的项目列表需要显示的时候&#xff0c;我们就可以使用 RecyclerView。 它具有重用其视图的能力。 在 Re…

【C语言】-程序编译的环境和预处理详解-让你轻松理解程序是怎么运行的!!

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 程序的编译前言一、 程序的翻译环境和执行环境二、 详解翻译环境2.1编译环境2.1.1预编…

代码随想录算法训练营第七天 | 454.四数相加II 、 383. 赎金信、15. 三数之和、18. 四数之和 、总结

打卡第七天&#xff0c;还是哈希表。 今日任务 454.四数相加II383.赎金信15.三数之和18.四数之和总结 454.四数相加II 代码随想录 class Solution { public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, ve…

单元测试面试秘籍分享

1. 什么是单元测试 “在计算机编程中&#xff0c;单元测试又称为模块测试&#xff0c;是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中&#xff0c;一个单元就是单个程序、函数、过程等&#xff1b;对于面向对象编程&#xff0c;最…

j-vxe-table 下拉搜索选择框数据加载过多导致前端崩溃问题

Jeeg-boot j-vxe-table 下拉搜索选择框数据加载过多导致前端崩溃问题 最近用到了Jeeg-boot j-vxe-table的组件&#xff0c;这组件时真J8难用&#xff0c;还好多BUG&#xff0c;想用个slot插槽也用不了&#xff0c;好像官方写了个基础就没怎么管了。&#x1f611; 问题&#xf…

google hacker语句

哎&#xff0c;我就是沾边&#xff0c;就是不打实战(&#xffe3;o&#xffe3;) . z Z 文章目录前言一、什么是谷歌Docker&#xff1f;二、受欢迎的谷歌docker语句谷歌docker的例子日志文件易受攻击的 Web 服务器打开 FTP 服务器SSH私钥电子邮件列表实时摄像机MP3、电影和 PDF…

php调试配置

错误信息输出 错误日志 nginx把对php的请求发给php-fpm fastcgi进程来处理&#xff0c;默认的php-fpm只会输出php-fpm的错误信息&#xff0c;在php-fpm的errors log里也看不到php的errorlog。原因是php-fpm的配置文件php-fpm.conf中默认是关闭worker进程的错误输出&#xff0…

【MySQL进阶】 锁

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…

Mybatis源码学习笔记(四)之Mybatis执行增删改查方法的流程解析

1 Mybatis流程解析概述 Mybatis框架在执行增伤改的流程基本相同&#xff0c; 很简单&#xff0c;这个大家只要自己写个测试demo跟一下源码,基本就能明白是怎么回事&#xff0c;查询操作略有不同&#xff0c; 这里主要通过查询操作来解析一下整个框架的流程设计实现。 2 Mybat…