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

news2025/2/23 17:41:53

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:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

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

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

相关文章

element分页

获取数据信息&#xff0c;这是表格和分页内容 <el-col :span"24"><div class"grid-content bg-purple-dark"><el-table :data"tableData" stripe style"width: 100%"><el-table-column prop"xuhao" l…

WorkPlus:企业数字化底座,统一数字化办公入口

在企业数字化转型的潮流下&#xff0c;统一入口的移动数字化底座成为了企业提高工作效率和迈向数字化时代的关键要素。在这个领域&#xff0c;WorkPlus凭借其独特的定位和功能&#xff0c;成为了企业微信、钉钉、飞书等类似产品中的完美选择&#xff0c;为企业提供了统一入口的…

旋转矩阵-数学理论

目录 概述 一、固定旋转&#xff08;Fix Angles&#xff09; 二、欧拉旋转&#xff08;Euler Angle&#xff09; 三、旋转矩阵小结 四、参考 概述 旋转矩阵是姿态的一种数学表达方式&#xff0c;或者笼统说变换矩阵是一种抽象的数学变量。其抽象在于当你看到…

【深度学习】卷积层填充和步幅以及其大小关系

参考链接 【深度学习】&#xff1a;《PyTorch入门到项目实战》卷积神经网络2-2&#xff1a;填充(padding)和步幅(stride) 一、卷积 卷积是在深度学习中的一种重要操作&#xff0c;但实际上它是一种互相关操作&#xff0c;&#xff0c;首先我们来了解一下二维互相关&#xff…

Redis主从配置和哨兵模式

主从简介 1、主从 – 用法 像MySQL一样&#xff0c;redis是支持主从同步的&#xff0c;而且也支持一主多从以及多级从结构。 主从结构&#xff0c;一是为了纯粹的冗余备份&#xff0c;二是为了提升读性能&#xff0c;比如很消耗性能的SORT就可以由从服务器来承担。 redis的主…

oracle-sql语句解析类型

语句执行过程&#xff1a;1. 解析(将sql解析成执行计划) 2.执行 3.获取数据(fetch) 1. shared pool的组成。 share pool是一块内存池。 主要分成3块空间。free&#xff0c; library(库缓存&#xff0c;缓存sql以及执行计划)&#xff0c;row cache(字典缓存) select * from v…

云贝教育 |【PostgreSQL PGCA】pg15安装pg_hint_plan扩展包

pg15安装pg_hint_plan扩展包 pg当前是支持HINT固定执行计划&#xff0c;需要通过扩展包pg_hint_plan来实现 一、扩展包下载&#xff1a; Releases ossc-db/pg_hint_plan GitHub 二、选择v15版本 pg_hint_plan15 1.5.1 is released pg_hint_plan15 1.5.1 is released. This…

SLAM从入门到精通(安全避障)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在工业生产中&#xff0c;安全是底线。没有安全性的技术&#xff0c;一般也不会在工业生产中进行部署、使用。对于slam来说&#xff0c;同样也是这…

机器人制作开源方案 | 行星探测车实现云端控制

1. 功能描述 本文示例所实现的功能为&#xff1a;手机端控制R261样机行星探测车的显示屏显示心形图。 2. 电子硬件 在这个示例中&#xff0c;我们采用了以下硬件&#xff0c;请大家参考&#xff1a; 3. 功能实现 编程环境&#xff1a;Milxy 0.999及以上版本 下面提供一个手机…

TerraNoise for 3dMax插件教程

TerraNoise for 3dMax插件教程 创建地形&#xff1a; - 从列表中拖动生成器并将其放到画布上 - 将导出器拖放到画布上 - 通过将一条线从生成器黄色输出“拖动”到导出器绿色输入来连接 2 个组件 - 单击导出器中的“无”按钮用于选择输出名称和格式&#xff08;导出 terragen 地…

Python装饰器的艺术

文章目录 装饰器基础示例代码:执行结果:参数化装饰器示例代码:执行结果:类装饰器示例代码:执行结果:装饰器的堆栈示例代码:执行结果:在Python中,装饰器是一种非常强大的特性,允许开发人员以一种干净、可读性强的方式修改或增强函数和方法。以下是一个关于Python装饰器…

聊聊定时器 setTimeout 的时延问题

给大家推荐一个实用面试题库 1、前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;web前端面试题库 全局的 setTimeout() 方法设置一个定时器&#xff0c;一旦定时器到期&#xff0c;就会执行一个函数或指定的代码片…

【开源】前后端分离后台管理系统

系统环境 JDK 17Maven 3.0.0MySQL 5.7.0Spring Boot 3.0.10 演示 橙子官网&#xff1a;http://hengzq.cnGitHub 代码下载&#xff1a;https://github.com/mmd0308/orangeGitee 代码下载&#xff1a;https://gitee.com/hengzq/orange 项目截图

Algorithms_LSM树(Log-Structured Merge Tree)

文章目录 引言1. LSM树的原理1.1 写入日志1.2 内存组件1.3 磁盘上的SSTable文件1.4 合并操作 2. LSM树的使用场景2.1 分布式数据库系统2.2 云存储系统2.3 日志和时间序列数据2.4 数据备份和归档 LSM VS BTree结论 引言 在当今信息时代&#xff0c;数据的存储和管理变得越来越重…

信息系统项目管理师第四版--风险管理--可搜索可编辑版本

1. 对目录进行了细化&#xff0c;从目录可以清晰看到每个过程的输入输出工具技术都有哪些&#xff0c;直接点击目录就可以跳转到相应的章节&#xff0c;免得自己在pdf文件里面一直翻一直翻&#xff0c;非常方便。是可编辑版本&#xff0c;还可以进行全文搜索。 下图是目录的部…

LoRaWAN物联网架构

与其他网关一样&#xff0c;LoRaWAN网关也需要在规定的工作频率上工作。在特定国家部署网关时&#xff0c;必须要遵循LoRa联盟的区域参数。不过&#xff0c;它是没有通用频率的&#xff0c;每个国家对使用非授权MHZ频段都有不同的法律规定。例如&#xff0c;中国的LoRaWAN频段是…

Centos7下搭建H3C log服务器

一、rsyslogH3C 安装rsyslog服务器 关闭防火墙 systemctl stop firewalld && systemctl disable firewalld关闭selinux sed -i s/enforcing/disabled/ /etc/selinux/config && setenforce 0centos7服务器&#xff0c;通过yum安装rsyslog yum -y install r…

如何用devtools快速开发一个R语言包?

如何用devtools快速开发一个R语言包&#xff1f; 1. 准备工作2. 如何完整开发一个R包3. 初始化新包4. 启用Git仓库5. 按照目标实现一个函数6. 在.R文件夹下创建文件并保存代码7. 函数测试8. 阶段性总结9. 时不时地检查完整工作状态10. 编辑DESCRIPTION文件11. 配置许可证12. 配…

vmware 启动qnx 环境下载配置

SDP QNX 安装手册 http://www.qnx.com/developers/docs/7.0.0/#com.qnx.doc.qnxsdp.quickstart/topic/install_host.html qnxsdp-6.5.0-x86-201007091524-nto.iso https://dude6.com/q/a/4990303.html http://www.qnx.com/download/feature.html?programid23647 vmarea …

【Linux】补充:进程管理之手动控制进程,以及计划任务

目录 一、手动启动进程 1、理解前台启动与后台启动 2、如何完成前台启动后台启动的切换 3、完成并行执行多个任务 4、结束进程 1、kill 2、killall 2、pkill 二、计划任务 1、at一次性计划任务 2、实操 2、周期性计划任务 1、关于设置周期性任务的配置文件以及格式…