Java并发编程第11讲——AQS设计思想及核心源码分析

Java并发包(JUC)中提供了很多并发工具,比如前面介绍过的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore、FutureTask等锁或者同步部件,它们的实现都用到了一个共同的基类——AbstractQueuedSynchronizer,简称AQS。本篇文章将深入剖析AQS的工作原理和核心概念,以理解多线程同步的关键技术。

一、什么是AQS

AQS全称AbstractQueuedSynchronizer。JDK 1.5之前只有synchronized同步锁,并且效率并不高,因此并发大神Doug Lea在JDK 1.5的时候自己写了一套框架,希望能够成为高效率地实现大部分同步需求的基础,也就是我们现在熟知的AQS(队列同步器)

AQS提供了一个同步器的框架,JUC包下大多数同步器都是围绕着AQS 使用的一组共同的基础行为(如等待队列、条件队列、独占或共享获取等)实现的,比如前边提到的ReentrantLock、CountDownLatch、Semaphore、FutureTask等,当然,我们也可以用AQS来构造出一个符合我们自己需求的同步器。

AQS支持两种同步方式:

  • 独占式(Exclusive):同一时刻只能有一个线程持有同步资源或锁。当一个线程成功获取到锁时,其它线程就必须等待,直到持有锁的线程释放资源才能继续执行,比如ReentrantLock。
  • 共享式(Shared):多个线程可以同时获取同一个同步资源或锁,从而实现并发方法。当一个线程获取到共享资源或锁后,其它线程仍然有机会获取资源,而不是被阻塞。比如CountDownLatch、Semaphore和CyclicBarrier就是一种共享锁。

二、AQS的常用方法与示例

AQS的设计是基于模板设计模式的,也就是说,使用者(子类)需要继承AQS并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用子类重写的方法。

2.1 可重写的方法

使用AQS的一般方式:

  • 继承AQS并重写指定的方法。(无非是对于共享资源state的获取和释放)
  • 将AQS组合在自定义同步组件中,并调用其模板方法,这些模板方法就会调用子类重写的方法,这是模板方法设计模式一个典型的应用。

需要注意的是,重写AQS指定方法的同时,需要使用同步器提供的下面三个方法来访问和修改同步状态:

  • getState():获取当前同步状态。
  • setState():设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态。

下面我们看看AQS定义的可重写的5个方法:

  • protected boolean tryAcquire(int arg):独占式获取同步状态,试着获取,成功返回true,失败返回false。
  • protected boolean tryRelease(int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
  • protected int tryAcquireShared(int arg):共享式获取同步状态,返回小于0的值表示获取失败,反之成功。
  • protected boolean tryReleaseShared(int arg):共享式释放同步状态,成功true,失败false。
  • protected boolean isHeldExclusively():是否在独占模式下被线程占用。

看过我之前文章的同学,除了最后一个方法,其它的是不是都很熟悉但又有点“模糊”,那么今天我们就一探究竟😊,看看到底是怎么个事。

2.2 常用方法

实现自定义同步组件时,将会调用同步器提供的模板方法,如下(部分):

  • void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则返回,否则将会进入同步队列等待,该方法会调用重写的tryAcquire(int arg)方法。
  • void acquireInterruptibly(int arg):与acquire方法相同,但是该方法响应中断,若线程未获取同步状态进入到同步队列,如果当前线程中断,则会抛出InterruptedException。
  • boolean tryAcquireNanos(int arg,long nanos):在acquireInterruptibly方法基础上增加了超时限制,如果超时,返回false,获取成功返回true。
  • void acquireShared(int arg):共享式获取同步状态,如果为获取,将进入同步列等待,与独占式获取的主要区别在于同一时刻可以又多个线程获取到同步状态。
  • boolean tryAcquireSharedInterruptibly(int arg):与acquireShared方法相同,可响应中断。
  • boolean tryAcquireSharedNanos(int arg,long nanos):共享模式获取,可中断,并且有超时时间。
  • boolean release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
  • boolean releaseShare(int arg):共享式获取同步状态。
  • Collection<Thread> getQueuedThreads():获取等待在同步队列上的线程集合。

2.3 基于AQS实现Mutex锁(示例)

上面大概讲了一下AQS的使用方式和常用的一些方法,接下来就借用JDK 1.8官方文档 在介绍AQS类时,举的一个例子来进一步理解AQS。

public class Mutex implements Serializable {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 当前状态为0的时候获取锁,CAS成功则将state修改为1
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将同步状态设置为0
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }
    //同步对象完成一系列复杂的操作,我们仅需指向它即可
    private final Sync sync = new Sync();
    //加锁,代理到acquire(模板方法)上,acquire会调用我们重现的tryAcquire方法
    public void lock() {
        sync.acquire(1);
    }
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    //释放锁,代理到release(模板方法上),release会调用我们重写的tryRealease方法
    public void unlock() {
        sync.release(1);
    }
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

上述示例中,独占锁Mutex是一个自定义的同步器,它在同一时刻只允许一个线程占有锁。接下来我们就是用常见的i++例子来检验一下Mutex:

public class TestMutex {
    private static int i = 0;
    private static Mutex mutex = new Mutex();
    //使用自定义的Mutex进行同步处理的a++
    public static void increase() {
        mutex.lock();
        i++;
        mutex.unlock();
    }
    public static void main(String[] args) throws Exception {
        //启动十个线程,每个线程累加10000次
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }).start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(i);//100000
    }
}

每次测试i的结果都是预期的100000,说明我们成功地基于AQS实现了一个简单的Mutex锁。

三、设计思想

AQS的设计思想实际很简单,可以分为三部分:同步状态的原子性管理(state)、队列的管理(CLH变体队列)以及线程的阻塞和释放(LockSupport),下面我们就逐个介绍一下。

3.1 同步状态的管理(state)

每个AQS的子类都依赖于一个volitile修饰的状态变量(state),可以通过getstatesetState以及compareAndSetState等方法进行操作,这个变量可以用于表示任意状态,比如ReentrantLock用它表示拥有锁的线程重复获取该锁的次数,CountDownLatch用它表示计数器的数值,Semphore用它表示剩余的许可数量,FutureTask用它表示任务的状态(尚未开始、正在运行、已完成和已取消)。

3.2 队列的管理

AQS最核心的就是队列的管理。AQS内部维护了两个内部类,分别是Node类(构建同步队列)和ConditionObject类(条件队列)。

3.2.1 同步队列(CLH变体队列)

AQS的核心思想就是如果被请求的共享资源(state的状态)空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制就是用CLH变体的虚拟双向队列实现的,即将暂时获取不到锁的线程加入到队列中。

我们先简单介绍下CLH队列:

CLH(Craig,Landin,and Hagersten——三个大佬的人名)队列,是单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱节点释放了锁就结束自旋。

暂时介绍这么多,今天我们的重点是AQS变体的CLH队列。

该队列由一个个Node节点组成,每个Node节点维护一个prev和next引用,分别指向自己的前驱和后继节点,AQS维护两个指针,指向队列头部head和尾部tail。

当线程获取资源失败时,就会构造成一个Node节点加入CLH变体队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现)。当持有同步状态的线程释放同步状态时,会唤醒(通过LockSupport.unpark实现)后继节点,然后此节点线程继续加入到对同步状态的争夺中。

3.2.2 条件队列

AQS内部提供了一个ConditonObject类,给维护独占同步的类以及实现Lock接口的类使用。

但是,有了CLH变种队列为什么还要条件队列呢?

因为CLH变种队列仅能解决线程阻塞和唤醒的问题,并不能提供条件和通知的功能。

因此,AQS引入了ConditionObject条件队列的概念,提供了一种更加高级的线程协作机制,能够更方便地实现特定条件的等待和唤醒。ConditonObject基于CLH变种队列实现,提供了信号通知、重入、公平性等特性,同时在使用时也更加方便和易于维护。

JUC包下的许多同步组件比如ReentrantLock、CyclicBarrier、Semaphore等,都有ConditionObject的身影。总之ConditionObject和CLH变种队列相辅相成,提供了一个完整、高效且灵活的线程协作机制,能够更好地支持更高级的线程同步操作。

3.3 线程的阻塞和释放(LockSupport)

在JSR166之前,阻塞和释放线程都是基于Java内置管程,唯一的选择的是Thread.suspendThread.resume,但之前的文章也提到过,由于存在死锁的风险,这两个方法都被声明废弃了。即:如果两个线程同时持有一个线程对象,一个尝试去中断,一个尝试去释放,在并发情况下,无论调用时是否进行了同步,目标线程都存在死锁的风险——如果suspend()中的线程就是即将要执行resume()的那个线程,那肯定就要产生死锁了。

JUC包有一个LockSuport类,它提供了另一种安全和可控的线程挂起和唤醒机制,以避免出现死锁和其它潜在问题:

  • 显式调用:使用LockSupport,线程的挂起和唤醒操作是显式的,需要开发者明确调用park和unpark方法。
  • 无需持有锁:LockSupport的park方法不会持有任何锁对象,因此不会引发死锁。线程在调用park方法挂起时,不会影响其他线程对锁的获取和释放操作。
  • 精确唤醒:LockSupport的unpark方法可以精确地唤醒指定的线程。与Thread的resume方法不同,unpark方法无需等待具体的操作,可以直接唤醒指定的线程。
  • 无状态变更:LockSupport的park和unpark方法不会导致线程状态的不一致性或其他潜在的问题。线程在被唤醒后,可以正常继续执行,遵循同步规则。

四、acquire和release方法源码分析

AQS里面最重要的就是两个操作和一个状态,即获取操作(acquire)、释放操作(release)和同步状态(state)。获取和释放操作又分为独占式共享式,这两种模式大同小异,所以今天就只对独占模式下的获取(acquire)释放(release)操作进行分析。

4.1 相关属性

再介绍之前我们先看一下相关的属性

static final class Node {
    //表示共享模式
    static final Node SHARED = new Node();
    //表示独占模式
    static final Node EXCLUSIVE = null;
    //表示线程已取消:由于在同步队列中等待的线程等待超时或中断
    //需要从同步队列中取消等待,节点进入该状态将不会变化(即要移除/跳过的节点)
    static final int CANCELLED =  1;
    //表示后继节点处于park,需要唤醒:后继节点的线程处于park,而当前节点
    //的线程如果进行释放或者被取消,将会通知(signal)后继节点。
    static final int SIGNAL    = -1;
    //表示线程正在等待状态:即节点在等待队列中,节点线程在Condition上,
    //当其他线程对Condition调用signal方法后,该节点会从条件队列中转移到同步队列中
    static final int CONDITION = -2;
     //表示下一次共享模式同步状态会无条件地传播下去
    static final int PROPAGATE = -3;
    //节点的等待状态,即上面的CANCELLED/SIGNAL/CONDITION/PROPAGATE,初始值为0
    volatile int waitStatus;
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //与当前节点关联的排队中的线程
    volatile Thread thread;
    //同步模式改变时下一个等待节点
    Node nextWaiter;
    //判断是否是共享模式,若是则返回true
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
        //返回节点的前驱节点,如果为null,则抛NullPointerException异常
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    //用于创建头节点或SHARED标记
    Node() {    // Used to establish initial head or SHARED marker
    }
        
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
       this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
       this.waitStatus = waitStatus;
       this.thread = thread;
    }
}
//同步队列的头节点,使用懒加载的方式初始化,仅能通过setHead修改。
private transient volatile Node head;
//同步队列的尾节点,同样是懒加载。仅通过enq方法修改,用于添加新的等待节点
private transient volatile Node tail;
//volatile修饰的状态变量state
private volatile int state;
//返回当前同步状态
protected final int getState() {
   return state;
}
//设置同步状态值
protected final void setState(int newState) {
   state = newState;
}
//使用CAS修改同步状态值
protected final boolean compareAndSetState(int expect, int update) {
   // See below for intrinsics setup to support this
   return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

如上所说,Node类其实就是构成CLH变体队列的一个个节点。

4.2 acquire方法

4.2.1 acquire()

分析:获取同步状态(锁)。

  • tryAcquire():调用的是子类重写的方法,若返回true,则表示获取同步状态成功,后面就不再执行,反之则进入下一步。
  • 此时,获取同步状态失败,构造独占式同步节点,通过addWaiter方法(见下)将此节点添加到同步队列尾部,并调用acquireQueued(见下)方法尝试acquire。
  • 最后,如果acquireQueued返回ture,则调用selfInterrupt方法中断当前线程。

4.2.2 addWaiter()

分析:根据当前线程和入参mode创建一个新Node,如果队列不为空则将Node设置为尾节点,反之则调用enq初始化队列并将node插入队列。

  • 第一个红框:以当前线程和mode为参数,创建一个节点node,将pred赋值为当前尾节点。

  • 第二个红框:pred不为空。

    • 将新创建的节点的前驱节点设置为pre,即将创建的节点放到尾部。

    • 使用CAS将尾节点修改为新节点。

    • 若修改成功,则将pred的后继节点设置为新节点,并返回新节点node。

  • 第三个红框:如果pred为空,则代表此时同步队列为空,调用enq方法(见下)将新节点添加到同步队列,并返回node。

4.2.3 enq()

分析:与上述的addWaiter方法相似,只是多了一个队列为空时,初始化head和tail的操作(懒加载)。

  • 第一个红框:

    • 将t赋值为尾节点。

    • 如果尾节点为空,使用CAS将头节点赋值为一个新创建的无状态节点,并初始化尾节点。

  • 第二个红框:如果尾节点不为空,使用CAS将当前node添加到尾节点。

    • 将node节点的前驱节点设置为t。

    • 使用CAS将尾节点设置为node。

    • 若设置成功,则修改node为t的后继节点,返回t。

4.2.4 acquireQueued()

分析:添加完节点后,立即尝试该节点是否能成功acquire。

  • 第一个红框:判断node节点的前驱节点p是否为头节点head,如果是则尝试acquire,若node成功acquire。则调用setHead方法将node设置为head、将node的Thread设置为null、将node的prev设置为null。将原头节点的next设置为null,也就是断开原head节点与node节点的关联,这就保证了头节点永远是一个不带Thread并且头节点的prev永远为null的空节点。

  • 第二个红框:如果node节点的前驱节点不是head,或者node尝试acquire失败,则会调用shouldParkAfterFailedAcquire方法(见下)检验node是否需要park,如果返回true则调用parkAndCheckInterrupt方法(见下)将node的线程阻塞。

  • 第三个红框:若failed为true,则代表出现了异常,调用cancelAcquire方法(见下)取消正在进行acquire的尝试。

4.2.5 shouldParkAfterFailedAcquire()

分析:判断节点是否需要park。

  • 第一个红框:判断前驱节点的等待状态是否为SIGNAL,若是,则表示该node应该park,等待其它前驱节点来唤醒。(此时的pred是原节点的前驱节点)

  • 第二个红框:

    • 如果前驱节点的等待状态大于0,也就是CANCELLED状态,也就是此节点已经无效,则需要从后往前遍历,找到一个非CANCELLED状态的节点,并将自己设置为它的后继节点。

    • 如果前驱节点的等待状态为其它状态,使用CAS尝试将pred节点的等待状态修改为SIGNAL,然后返回false。这就意味着再执行一次acquireQueued方法的第一个if,再次tryAcquire。

4.2.6 parkAndCheckInterrupt()

分析:直接调用LockSupport的park方法将当前线程阻塞,并在被唤醒之后,返回当前线程是否中断。

4.2.7 cancelAcquire()

分析:取消正在等待获取独占同步状态的线程。

  • 第一个红框:首先判断传入的节点是否为空,为空就直接返回,不为空就将node的Thread设置为null。

  • 第二个红框:如果node的前驱节点的等待状态为CANCELLED,则直接断开与该节点的联系。

  • 第三个红框:拿到pred的后继节点predNext(不一定是node了),并将node的等待状态设置为CANCELLED。如果node为尾节点,则CAS将尾节点改为pred节点,也就是把pred后面的节点全部移除(包括node节点和node节点前面等待状态为CANCELLED的节点)。

  • 第四个红框:后继节点的唤醒和更新

    • (判断当前节点是否为头节点)&&((获取node的前驱节点的等待状态赋值给ws并判断其等待状态是否为SIGNAL)||(判断ws是否是除CANCELLED状态之外的状态 && 如果是则将其状态设置为SIGNAL ))&& 判断node的前驱节点的线程是否不为null

    • 如果上述条件都满足,获取当前节点的后继节点next,如果next不为空且等待状态不为CANCELLED,则将前驱节点的后继节点设置为后继节点的后继节点,即跳过当前节点,因为只有pred的等待状态为SIGNAL才能走到这边,因此node的后继节点无需唤醒。

    • 反之,如果pred节点无法提供给node的后继节点信息,则直接唤醒node的后继节点(调用unparkSuccessor方法(见下))。

    • 最后置空当前节点的引用,便于垃圾回收。

4.2.8 unpakSuccessor()

分析:唤醒node节点的后继节点

  • 第一个红框:将node节点的等待状态赋值给ws,如果ws小于0(即等待状态不是CANCELLED),则将ws的等待状态置为0(初始状态因为马上要将node的后继节点唤醒)。

  • 第二个红框:将node节点的后继节点赋值给s,如果s==null或者s的等待状态为CANCELLED,则直接将s置空,并从队列尾部向前遍历,找到等待状态不是CANCELLED的节点t(离node最近的节点),并将其赋值给s。这里的意思就是将node之后的空节点或等待状态为CANCELLED的节点也一并去掉,直接唤醒node之后等待状态不为CANCELLED的节点。

  • 第三个红框:如果s!=null,则执行LockSupport.unpark(s.thread)唤醒s节点。

4.3 release()方法

分析:释放同步状态。

  • tryRelease():首先调用子类重写的tryRelease()方法,尝试释放锁。

  • 如果tryRelease()成功即释放锁成功,并且head节点不为空且等待状态不是初始状态,则调用unparkSuccessor方法(见4.2.8)唤醒head节点的后继节点。

4.4 acquire方法总结

release方法简单没什么好总结的,这里就总结一下acquire方法。

  • 首先,acquire方法会调用tryAcquire方法尝试直接获取锁。这个方法是由子类实现的,用于决定是否允许当前线程获取锁。如果tryAcquire方法成功获取了锁,就直接返回。
  • 如果tryAcquire方法无法直接获取锁,当前线程会通过调用addWaiter方法将该线程添加到等待队列中,如果队列不为空则将node放置队列尾部,如果为空则调用enq方法初始化队列,并放置队列尾部。
  • 接下来会调用acquireQueued方法,线程进入自旋状态,期间会不断尝试获取锁。首先会检查该节点的前驱节点是否为head节点,如果是则意味着当前节点是老二节点,可以再次调用tryAcquire方法尝试获取锁,如果获取锁成功,那么它将成为head节点,并将head节点的前驱节点置为null。如果不是头节点或者获取锁失败,则会调用shouldParkAfterFailedAcquire方法判断当前是否需要park。
  • 如果在acquireQueued中发生异常,则会执行cancelAcquire方法取消正在等待获取独占同步状态的线程。
  • 最后如果acquireQueued方法返回true,则调用selfInterrupt方法中断当前线程,这是因为返回ture就代表线程被中断。

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

汽车网络安全渗透测试概述

目录 1.汽车网络安全法规概述 1.1 国外标准 1.2 国内标准 2.汽车网络安全威胁分析 2.1 汽车网络安全资产定义 2.2 汽车网络安全影响场景及评级示例 3.汽车网络安全渗透测试描述 3.1 参考法规 3.2 渗透测试内容 4.小结 1.汽车网络安全法规概述 近年来&#xff0c;汽车…

【UE4】UE编辑器乱码问题

环境&#xff1a;UE4.27、vs2019 如何解决 问题原因&#xff0c;UE的编码默认是UTF-8&#xff0c;VS的默认编码是GBK 通过"高级保存选项" 直接修改VS的 .h头文件 的 编码 为 UTF-8 步骤1. 步骤2. 修改编码后&#xff0c;从新编译&#xff0c;然后就可以解决编辑器…

很多个pdf怎么合并在一起?

很多个pdf怎么合并在一起&#xff1f;作为一个办公室的伙伴&#xff0c;对于PDF格式肯定不会陌生。它强大的功能为我们的工作提供了许多便利。由于PDF文件格式的稳定性和安全性较高&#xff0c;我们通常在工作或学习中使用它来传输文件&#xff0c;很多人都喜欢将办公文件都做成…

Java用log4j写日志

日志可以方便追踪和调试问题&#xff0c;以前用log4net写日志&#xff0c;换Java了改用log4j写日志&#xff0c;用法和log4net差不多。 到apache包下载下载log4j的包&#xff0c;解压后把下图两个jar包引入工程 先到网站根下加一个log4j2.xml的配置文件来配置日志的格式和参…

自动驾驶行业观察之2023上海车展-----车企发展趋势(3)

合资\外资发展 宝马&#xff1a;i7、iX1新车亮相&#xff0c;未来将持续发力电动化、数字化&#xff08;座舱&#xff09; 宝马在本次车展重点展示了电动化产品&#xff0c;新发车型为i7 M70L、iX1、及i vision Dee概念车等车型。 • 展示重点&#xff1a;电动化数字化&#…

STM32H750之FreeRTOS学习--------(五)临界段代码保护

FreeRTOS 文章目录 FreeRTOS五、临界段代码保护临界段代码保护函数任务级进入临界段任务级退出临界段中断级进入临界段中断级退出临界段 任务调度器的挂起和恢复挂起任务调度器恢复任务调度器挂起任务调度器恢复任务调度器 五、临界段代码保护 临界段代码也叫做临界区&#xf…

Android EditText 实现强制性弹出只能输入英文的键盘

如果 EditText 控件不做任何特殊处理&#xff0c;例如笔者手机默认弹出的是百度输入法的软键盘&#xff0c;可实现中英文切换&#xff0c;并且自带英文单词智能联想功能&#xff08;与系统安装输入法和设置相关&#xff09;。但在某些应用场景下&#xff0c;例如在英语APP里练习…

RabbitMQ(高级特性):限流

消费端限流 在rabbitmq中&#xff0c;使用消费端限流必须开启手动签收信息 过MQ可以对请求进行“削峰填谷”&#xff0c;即通过消费端限流的方式限制消息的拉取速度&#xff0c;达到保护消费端的目的。 生产者批量发送消息&#xff1a; Test public void testSendBatch() {…

DBever 连接trino时区问题 The datetime zone id ‘GMT+08:00‘ is not recognised

DBever连接trino 测试连接成功&#xff0c;但是执行sql报时区不对、如果你默认使用的是大于jdk8的版本 会存在这个问题&#xff0c;因为jdk版本 jdk8 和jdk17 版本默认时区是不同的 trino官网明确说明了时区默认跟jdk走 解决方案 可以先行查看JDK本地时区库版本&#xff0c;执…

Django初窥门径-自定义用户模型

前言 自定义用户模型在Django应用中是一个重要的话题&#xff0c;它涉及到如何根据您的项目需求以及特定的用户身份验证和授权需求来调整用户模型。在以下前言中&#xff0c;我将讲述为什么自定义用户模型是如此重要以及其潜在的优势&#xff1a; 随着Web应用的不断发展&…

工业路由器网关的网络协议之NAT技术

在物联网通讯领域&#xff0c;NAT技术能将内网的一个私有IP转换成一个公网IP去接入互联网&#xff0c;解决组建局域网络时私有IP地址无法在公网上进行路由的问题。 NAT&#xff08;Network Address Translation&#xff09;的三种方式&#xff1a; 静态NAT 1、一个私有IP对应…

在微信小程序中怎么实现刮刮卡抽奖活动

在当今的数字化时代&#xff0c;微信小程序已经成为一种广泛使用的营销工具。通过各种互动活动&#xff0c;企业可以吸引用户的关注&#xff0c;提升品牌影响力。其中&#xff0c;刮刮卡抽奖活动是一种特别受欢迎的形式。本文将为你详细介绍如何在微信小程序中实现刮刮卡抽奖活…

Python中通过socketserver库创建服务端

socketserver库是Python的标准库&#xff0c;提供了套接字服务端的框架&#xff0c;通过该框架可以简化服务端的创建流程。 1 socketserver库的导入 通过如图1显示的代码导入socketserver库。 图1 导入socketserver库 2 通过socketserver库创建TCP服务端 通过socketserver库…

SpringCloud——服务容错——Hystrix

1.现在的微服务存在哪些问题&#xff1f; 在大型的微服务项目中&#xff0c;肯定少不了服务之间多条链路调用&#xff0c;如果调用中有一个服务出现了问题&#xff0c;如果不做任何的处理&#xff0c;就会造成大量的阻塞&#xff0c;可能会导致整个服务雪崩。 2.要解决的问题 …

C盘清理指南(三)——文件目录更改

各位小伙伴你们好&#xff0c;今天的推送是C盘清理系列的第三期——文件路径更改&#xff0c;分为文件夹路径和软件默认路径两个模块。 一&#xff0e;文件夹路径更改 点击进入C盘&#xff0c;依次点击上方“查看——隐藏的项目”&#xff0c;可以看到C盘中各种隐藏目录。 单击…

深度图(Depth Map)

文章目录 深度图深度图是什么深度图的获取方式激光雷达或结构光等传感器的方法激光雷达RGB-D相机 双目或多目相机的视差信息计算深度采用深度学习模型估计深度 深度图的应用场景扩展阅读 深度图 深度图是什么 深度图&#xff08;depth map&#xff09;是一种灰度图像&#xf…

使用 curator 连接 zookeeper 集群 Invalid config event received

dubbo整合zookeeper 如图&#xff0c;错误日志 2023-11-04 21:16:18.699 ERROR 7459 [main-EventThread] org.apache.curator.framework.imps.EnsembleTracker Caller0 at org.apache.curator.framework.imps.EnsembleTracker.processConfigData(EnsembleTracker.java…

为机器学习算法准备数据(Machine Learning 研习之八)

本文还是同样建立在前两篇的基础之上的&#xff01; 属性组合实验 希望前面的部分能让您了解探索数据并获得洞察力的几种方法。您发现了一些数据怪癖&#xff0c;您可能希望在将数据提供给机器学习算法之前对其进行清理&#xff0c;并且发现了属性之间有趣的相关性&#xff0c…

Mysql学习笔记--基础

一&#xff0c;SQL最重要的增删改命令格式 1&#xff0c;insert into 表名&#xff08;不写这个括号里面的内容就默认所有字段都要添加&#xff09; values&#xff08;&#xff09; 插入单条数据 2&#xff0c;insert into 表名 (里面是列名) values&#xff08;根据列名依次…

【qemu逃逸】华为云2021-qemu_zzz

前言 虚拟机用户名&#xff1a;root 无密码 设备逆向 经过逆向分析&#xff0c;可得实例结构体大致结构如下&#xff1a; 其中 self 指向的是结构体本身&#xff0c;cpu_physical_memory_rw 就是这个函数的函数指针。arr 应该是 PCI 设备类结构体没啥用&#xff0c;就直接用…