Java线程池ThreadPoolExecutor源码解析

news2025/7/5 17:36:39

Java线程池ThreadPoolExecutor源码解析

1.ThreadPoolExecutor的构造实现

以jdk8为准,常说线程池有七大参数,通常而言,有四个参数是比较重要的

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

  • corePoolSize:核心线程数,具体含义理解代码

  • maximumPoolSize:最大线程数

  • keepAliveTime:线程空闲的存活时间

  • unit:时间单位

  • BlockingQueue:阻塞队列,用来保存等待执行的任务

接下来去看完整参数的构造实现:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

  • ThreadFactory:线程工厂,用来创造线程

  • RejectedExecutionHandler:拒绝策略

  1. 如果核心线程数等其他参数非法化就会抛出相应的异常

 if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();

  1. 之后进行初始化赋值

 this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;

  • 注:acc是一个成员变量,用来管理线程池中线程的访问控制上下文,其实现类是AccessControlContext


2.线程池的执行execute

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

总共大致分为三步:要想理解线程池的执行,要先去理解控制字段其具体含义

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

先声明线程池的五种状态,再看其他字段方法具体执行了其他什么操作

  • RUNNING: -1 << COUNT_BITS,即高3位为111

  • SHUTDOWN: 0 << COUNT_BITS,即高3位为000

  • STOP : 1 << COUNT_BITS,即高3位为001

  • TIDYING : 2 << COUNT_BITS,即高3位为010

  • TERMINATED: 3 << COUNT_BITS,即高3位为011

至于其每种空置状态的具体意义,根据英文释义结合代码具体理解,而非直接理解,通过位移位的操作将高3位与低29位分离开来,高三位表示此时整个线程池的运行状态,低29位表示线程池中线程的数量,再去看execute执行过程即可.

int c = ctl.get(); 
if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

  • 用于获取此时线程中的线程数,如果小于核心线程数,就添加任务,添加任务成功则返回,失败则重新获取控制字段,addworker后续了解,复杂的东西简单化,理解大致操作思想最为核心.

if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

  • 根据控制字段c去判断线程池的运行状态是否正在运行,如果添加任务成功则不会执行失败,或者说此时线程数有可能已经大于了核心线程数也有可能走到这,所以会将任务添加到阻塞队列中去,然后重新去获得控制字段,再去做校验,如果此时线程池不是正在运行的状态并且删除任务成功,这一步主要是为了防止阻塞队列添加任务成功这个过程,可能线程池不运行了,那么这时候就需要将添加的那个任务删除,并对他执行拒绝策略,又或者是此时线程池中的线程数已经为0,说明没有线程在工作了,因此添加一个空任务,至于第二个参数在addWorker中在做说明

else if (!addWorker(command, false))
            reject(command);

  • 字面意思就是添加任务失败,执行拒绝策略,则是为了应对线程池已经到了满负载的状态


3.线程池的任务添加addworker

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

代码量很长,但是大致可分为两部分,且逻辑很清晰

  1. 这里使用了标签语法,前半段大致是是否需要添加线程做一系列准备

 retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

  1. 获取控制字段c:这个字段包含了运行状态信息和线程池数量信息,是一个复合字段,而rs则是获取高三位的线程池状态信息

  1. 根据前面线程池状态信息,运行RUNNING值最小,因此判断线程池如果处于非运行的状态,则去判断是否处于关闭的状态,判断第一个任务是否为空,队列不为空,但是由于前面取反操作,其真正含义是:如果线程池的状态不是 SHUTDOWN,或者任务队列为空,或者有待执行的任务,那么就不会拒绝新任务的提交,否则就返回false,表示添加任务失败

  1. 接下来死循环表示需要去添加执行的任务,首先获取线程池中的线程数,关键的地方在这,如果此时的线程数大于等于容量或者(这里根据传递的参数core来选则比较的目标是核心线程数还是最大线程数),比较失败,则说明超过了接受的范畴,添加任务失败,如果没有失败,则通过底层CAS操作使得线程数加1,然后直接结束调用,跳出循环,,如果CAS失败,则说明ctl字段受到了变化,此期间有其他任务参与,重新获取此字段,去判断一下重新获取的ctl字段和之前的rs字段是否相等,这是为了保障多线程情况下出现的一种并发竞争问题导致的线程数发生错乱.

  • 至此,上半部分的核心已经解决,下半部分此时真正去实现任务的添加,通过线程池中的内部类Worker去实现

  boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

两个布尔类型暂时不用管,大致猜测意思即可,将firstask任务交付给worker,由worker内部的thread线程去执行,因此需要去理解worker的实现

3.1Worker内部的工作者

3.11构造方法实现

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

接收一个Runnable参数做为任务进行初始化,这里用到了AQS的一些实现,然后通过线程工厂创造一个新的线程,赋予给内部的成员变量引用

  • 还有一些锁的一些操作,后续再看

  1. 如果工作者的内部线程已经被创造好,实现就绪,要先获得线程池的互斥锁,然后对接下来的操作进行互斥访问

  1. 重新获取最新的线程池的运行状态,只有当线程池处于运行状态或者处于关闭状态但没有待执行的任务时,才能将新工作线程添加到线程池中,也就是worker中去,因此一个worker内部具备一个thread,如果想要实现许多线程去完成线程池的相应操作,需要将worker封装成集合,因此线程池内部还有一成员变量:

private final HashSet<Worker> workers = new HashSet<Worker>();

这样就确保了每一个worker都是独一无二的,不会重复的,也就意味着每一个线程都不一样.

  1. 而最后一个largestPoolSize则是保留历史的最大线程数的,用来记录,至此就已经添加成功了,只不过此时还没有执行

  1. 之后解锁,用之前标志位workerAdded表示添加成功,然后启动线程,也就是去执行这个任务,再用另一个标志workerStarted表示启动成功.

  1. 最后则是检查是否有什么异常在启动期间,如果没启动成功,则调用addWorkerFiled方法去处理

3.12 addWorkerFailed方法

private void addWorkerFailed(Worker w) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (w != null)
            workers.remove(w);
        decrementWorkerCount();
        tryTerminate();
    } finally {
        mainLock.unlock();
    }
}

  • 紧接上文,也就是启动失败的话,会将执行任务的workerremove(底层通过HashMap实现键的删除),然后减少线程数,等待一会,这个过程是互斥的,因为牵扯到控制字段

  1. 至此,添加任务如果成功,则进行执行,如果成功开启执行,则成功返回

因此,根据线程池的执行添加流程,大致可以将此过程通过绘图的方式表现出来:

线程池


4.工作者的run方法是如何运行的

worker中还有一个方法

  public void run() {
            runWorker(this);
        }

是其执行的具体操作

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

  • 上锁之前的操作都很容易看懂,处了getTask,这个方法用来获取阻塞队列中的任务,后续再理解

  1. 首先看第一段if,就是用来查看此时线程池的状态,如果不处于关闭或者运行的状态,或者线程处于中断的状态,则确保线程中断

  1. 接下来是一部分异常和错误的处理以及执行一些前置任务和一些后置任务

  1. 最后完成的任务数加一,解锁,将标志位是否中断,改为false,表示执行成功.


5.获取任务的getTask方法

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

  1. 首先标志位用来判断是否超时,默认情况下不超时,跟之前的参数挂钩,后续再看,然后进入死循环,不断循环去执行后续操作

  1. 获取控制位c和rs运行状态,之后的if操作含义是如果线程池处于关闭的状态或者此时队列为空,就说明没有任务需要处理了,此时让线程池中的线程数减一,返回,另一种情况则是线程池的状态处于关闭状态之上,则说明线程池现在不执行任务了,不需要管队列中是否还有任务存在,则同上减一,返回。

  1. 然后重新获取线程池的线程数,接下来的time布尔这个字段有些作用,后面的allowCoreThreadTimeOut是一个控制字段,用来表示线程是否允许超时而返回的一个字段,如:线程池中的核心线程如果因为长时间没有得到任务的滋养,就如同线程之间会发生饥饿一样,因此存在一个字段用来控制超时是否生效.因此如果线程数大于核心线程数或者开启超时控制字段,就说明会执行超时退出.

  1. 接下来的if判断是用来执行递减线程数的一个操作,底层采取CAS就不多说了,wc > maximumPoolSize:用来表示如果大于了最大线程数,说明需要减少线程数,至于为什么会出现这种情况,等会理解.(timed && timedOut):说明开启超时退出,且上一次获取任务因超时返回,这个需看后面代码理解.上面的两个条件满足其中之一即可.wc > 1 || workQueue.isEmpty())而这个操作则是为了减少不必要的线程开销,如果阻塞队列为空说明没有任务,那自然不需要多余的线程数去执行,因此会发生接下来的操作,递减线程数,然后跳到下一次循环.

  1. 之后接下来就是从阻塞队列中获取任务的核心了,第一步是根据超时控制字段来决定行为方式,允许超时退出的话,通过poll方式,不允许则通过take方式,两种方式大致是一个等待一定时间,如果为空是前提.另一个是无限等待,会阻塞线程.其具体实现通过阻塞队列的真正实现类别去实现.如果获取到了任务,就返回,如果没有则timeout设置为true,表示没有接受到任务,因此前文的timeout就理解了.

  1. 通常而言线程池中的线程数是不允许超过最大线程数的,但通常而言这是一种机制的完整性和规范,假如是自定义线程池的情况下,就有可能出现这种情况,另外一种是本人推测虽然由于增加工作线程数的操作底层是通过CAS去实现的,底层是原子性的,同时进行CAS操作就有可能导致ABA问题出现,或者操作失败,或者不断自旋的可能,


6.任务的提交submit

众所周知,任务需要进行提交给线程池,再有线程池去执行,而Runnable接口实现的run方法是没有返回值的,而在线程中Callable通常具备返回值,且配备Future去接受结果.因此submit具备不同的操作

这里以AbstractExecutorService(线程池的父类)接口为例:

public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

 public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

  • RunnableFuture接口的实现类FutureTask,总而言之就是转换为一个Runnable,然后进行提交,最后返回一个future,至于FutureTask具体内容自行详解.


7.线程池的关闭

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

实现逻辑也很清楚,检查是否可以关闭线程,然后设置线程的状态,interruptIdleWorkers()这个方法算是关键的,他会去中断worker;onShutdown是一个空方法,留给子类去实现的.

 private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
    }

private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

他会去遍历集合workers,获取每一个worker的工作线程,然后尝试去中断,最后结束.

文章转载自:不会上猪的树

原文链接:https://www.cnblogs.com/blissful/p/17930818.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

python3处理xls并flask显示

前言&#xff1a; 工作中有需求对xls文件进行生成和显示&#xff0c;这里就将对应的xls文件的处理&#xff0c;读取和flask展示代码罗列出来&#xff0c;方便大家使用&#xff1a; 需要的模块&#xff0c;这里需要注意版本号&#xff0c;如果直接安装使用报错&#xff0c;就指…

Tensorflow2.X的GPU版框架最快最稳搭建方法

一、环境基础 Windows10以上 已装Anaconda 支持GPU 二、搭建步骤 1. 在Anaconda中创建并进入虚拟环境 conda create -n envname python3.8 conda activate envname 注意&#xff1a;envname 替换为你自己想命名的&#xff0c;下文将以“Ljdenv”出现 2.安…

微服务实战系列之Dubbo(下)

前言 眼看着2023即将走远&#xff0c;心里想着似乎还有啥&#xff0c;需要再跟各位盆友叨叨。这不说曹操&#xff0c;曹操就来了。趁着上一篇Dubbo博文的余温尚在&#xff0c;博主兴匆匆地“赶制”了Dubbo的下集&#xff0c;以飨读者。 上一篇博主依然从Dubbo的内核出发&#…

几代WiFi有什么差异,它们有什么区别

最典型的差异指标&#xff1a;单流传输速率 第一代 基于的标准&#xff1a; 802.11 使用频率&#xff1a;2.4GHz 单流最大传输速率&#xff1a;2Mbit/s 第二代 基于的标准&#xff1a; 802.11b 使用频率&#xff1a;2.4GHz 单流最大传输速率&#xff1a;11Mbit/s 第三代 …

python文件打包实战技巧

众所周知&#xff0c;python是一种脚本语言&#xff0c;python程序必须在python环境下运行&#xff0c;所以如果想把自己写的程序给别人看的话&#xff0c;就比较麻烦&#xff0c;他需要先配置python环境&#xff0c;对于电脑小白来说这是“要命”的事情。而且如果是客户的话&a…

【SD】inpaint 模型 - 换脸术 ☑

文生图-局部重绘 涂抹脸部 关键词添加lora&#xff1a; <lora:Naruto_zilaiye:1.5>, 生成图&#xff1a;

Python 中的运算符介绍(1)

算数运算符 常见的% 、//、/ 用法 赋值运算符 赋值运算&#xff1a;将等号右边赋值给等号左边 常见场景&#xff1a; 比较运算符 代码解析&#xff1a; 逻辑运算符 位运算符&#xff08;了解&#xff09; 三目运算符 身份证运算符 成员运算符

uniApp中uView组件库的丰富单元格方法

目录 基础功能 #自定义内容 #自定义大小 #展示右箭头 #跳转页面 #右侧内容垂直居中 #自定义插槽 当谈到uniapp的uView组件库中的单元格&#xff08;Cell&#xff09;方法时&#xff0c;我们需要了解它的基本功能和用法。 单元格是一种常用的UI元素&#xff0c;在移动应…

每日一题——LeetCode977

方法一 个人方法&#xff1a; 以示例1为例&#xff1a;把[-4,-1,0,3,10] 中n<0的元素拆分出来&#xff0c;把他们的平方从小到大放入arr数组&#xff0c;则arr[0,1,16] ,那数组就还剩[3,10] 对于剩下的元素&#xff0c;看arr里面有没有比他们平方更小的元素先放入res数组&…

WAZUH的安装、设置代理

wazuh安装 wazu的安装分为以下两种方式 官方文档&#xff1a;https://wazuh.com/blog/detecting-common-linux-persistence-techniques-with-wazuh/ 1、自定义安装 这种方式就是一步一步的安装 直接参考官方文档&#xff1a; 这里就不详细介绍了 2、直接下载打包好的虚拟机…

python观察图像的幅度谱和相位谱——冈萨雷斯数字图像处理

原理 在图像处理中&#xff0c;当我们对图像进行傅里叶变换&#xff0c;可以得到两个重要的成分&#xff1a;幅度谱和相位谱。这些成分在图像分析和处理中扮演着关键的角色。 傅里叶变换 傅里叶变换是一种将信号从时域&#xff08;或空间域&#xff09;转换到频率域的工具。对…

Note: Wildlife Protection

wildlife protection protection wildlife Dinosaurs died out because of an unexpected incident. unexpected dinosaurs But wildlife today disappears or is in danger just because humans do harm to it. 但是&#xff0c;今天的野生动植物因为人类的伤害而消失了或…

Hadoop安装笔记1单机/伪分布式配置_Hadoop3.1.3——备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

将下发的ds_db01.sql数据库文件放置mysql中 12、编写Scala代码&#xff0c;使用Spark将MySQL的ds_db01库中表user_info的全量数据抽取到Hive的ods库中表user_info。字段名称、类型不变&#xff0c;同时添加静态分区&#xff0c;分区字段为etl_date&#xff0c;类型为String&am…

Autodesk Maya各版本安装指南

链接地址如下&#xff1a; https://pan.baidu.com/s/1Fg7MvUJS0tl5t2XAwMK9xg?pwd0531 1.鼠标右击【Maya2024(64bit)】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;【解压到 Maya2024(64bit)】。 2.打开解压后的文件夹&#xff0c;双击打开【Setu…

【形式语言与自动机/编译原理】CFG->Greibach->NPDA(1)

本文将详细讲解《形式语言与自动机》&#xff08;研究生课程&#xff09;或《编译原理》&#xff08;本科生课程&#xff09;中的上下文无关文法&#xff08;CFG&#xff09;转换成Greibach范式&#xff0c;再转成下推自动机&#xff08;NPDA&#xff09;识别语言是否可以被接受…

SpringValidation自定义注解以及分组校验

SpringValidation的参数校验使用可参考&#xff1a;【SpringMVC应用篇】Spring Validation 参数校验-CSDN博客 目录 1. 引入依赖 2. 自定义注解校验 2.1 创建Validation类 2.2 创建注解对象 2.3 使用注解 3. 分组校验 3.1 实体类内部定义接口 3.2 在参数上指定分组 1. …

这一次,我准备了 20节 PyTorch 中文课程

对于刚接触深度学习的小白来说&#xff0c;PyTorch 是必会的框架。 只是&#xff0c;很多小伙伴还没来得及开启学习之路&#xff0c;一个最重要的问题就摆在了面前&#xff1a; PyTorch&#xff0c;该怎么学呢&#xff1f; 很多同学会自己在网上找资料&#xff0c;不仅耗费时间…

Unity中裁剪空间推导(使用FOV来调节)

文章目录 前言一、使用FOV代替之前使用的Size&#xff08;h&#xff09;1、我们可以把矩阵中使用到 h(高) 和 w(宽) 的部分使用比值替换掉。2、替换后 前言 在之前的文章中&#xff0c;我们控制透视相机使用的是SIze。但是&#xff0c;在透视相机中&#xff0c;我们使用的是FO…

webrtc turn服务器搭建

测试环境ubuntu 22LTS 首先从github上下载源码编译 GitHub - coturn/coturn: coturn TURN server project 用的tag docker/4.6.2-r7 ./configure --prefix /usr/local/coturn make 安装coturn的时候还需要安装一些依赖包 apt-get install pkg-config apt-get install op…

图片处理软件,批量缩放图片尺寸

无论是社交媒体上的分享&#xff0c;还是工作中的图片素材&#xff0c;往往大小不一、形状不同&#xff0c;这时如何批量高效地对这些图片进行缩放就显得尤为重要。传统的图片处理软件&#xff0c;不仅操作复杂&#xff0c;而且处理速度慢&#xff0c;更无法实现批量化处理。但…