ThreadPoolExecutor工作原理及源码详解

news2024/9/21 22:56:17

一、前言

创建一个线程可以通过继承Thread类或实现Runnable接口来实现,这两种方式创建的线程在运行结束后会被虚拟机回收并销毁。若线程数量过多,频繁的创建和销毁线程会浪费资源,降低效率。而线程池的引入就很好解决了上述问题,线程池可以更好的创建、维护、管理线程的生命周期,做到复用,提高资源的使用效率。也避免了开发人员滥用new关键字创建线程的不规范行为。

说明:阿里开发手册中明确指出,在实际生产中,线程资源必须通过线程池提供,不允许在应用中显式的创建线程。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

通过分析ThreadPoolExecutor线程池的工作原理、核心参数以及执行过程来深入了解线程池相关工作原理。

二、ThreadPoolExecutor

ThreadPoolExecutor是java中实现线程池的核心类,主要用于管理和异步执行任务。从以下几个方面分析工作原理

1、生命周期

线程存在生命周期,同样线程池也有生命周期。生命周期中存在5个状态。

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;

线程池的开启到关闭过程就是这5个状态流转过程,状态之间转换流转如下图。

5个状态描述说明如下

状态

含义

RUNNING

运行状态,该状态下线程池可以接受新的任务,也可以处理阻塞队列中的任务

执行 shutdown 方法可进入 SHUTDOWN 状态

执行 shutdownNow 方法可进入 STOP 状态

SHUTDOWN

待关闭状态,不再接受新的任务,继续处理阻塞队列中的任务

当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态

STOP

停止状态,不接收新任务,也不处理阻塞队列中的任务,并且会尝试结束执行中的任务

当工作线程数为0时,进入 TIDYING 状态

TIDYING

整理状态,此时任务都已经执行完毕,并且也没有工作线程

执行 terminated 方法后进入 TERMINATED 状态

TERMINATED

终止状态,此时线程池完全终止了,并完成了所有资源的释放

线程池的重点之一就是控制线程资源合理高效的使用,所以必须控制工作线程的个数,所以需要保存当前线程池中工作线程的个数。ThreadPoolExecutor中使用一个AtomicInteger类型的ctl属性存储了线程池的状态和工作线程个数。

ctl的高3位用来表示线程池的状态(runState),低29位用来表示工作线程的个数(workerCnt)。因为线程池有5个状态,2位只能表示4个状态,所以用3位来表示5个状态。

2、创建线程池

1. 构造方法

通过ThreadPoolExecutor类的构造方法,来创建一个线程池,其中构造方法上有7大核心参数,通过7大核心参数的配置来定制化线程池。

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;
    }

7大核心参数含义:

corePoolSize:核心线程数

maximumPoolSize:最大线程数

keepAliveTime:非核心线程空闲时间

unit:keepAliveTime的时间单位

workQueue:工作阻塞队列

ThreadFactory:线程工厂提供创建线程的方式,默认使用Executors.defaultThreadFactory()创建。

handler:拒绝策略

2. 核心参数详解

ThreadPoolExecutor的构造方法中包含了7大核心参数,通过配置核心参数可以定制化线程池。接下来分别介绍参数

corePoolSize 和 maximumPoolSize

通过corePoolSize和maximumPoolSize在构造方法中设置线程池边界,来调整线程池中的工作线程数量。工作线程个数自动调整有两个场景:

a. 工作线程个数小于corePoolSize

当线程池提交一个新任务,且工作线程个数小于corePoolSize时,即使其他工作线程空闲,也会创建一个新线程来执行任务

b. 线程数量介于corePoolSize和maximumPoolSize之间

当运行的工作线程数量大于corePoolSize但小于maximumPoolSize,且阻塞队列已满时 才会创建非核心工作线程。

默认情况下,即使是核心线程也是在新任务到达时开始创建和启动的。若使用非空队列创建线程池,可通过重写prestartCoreThread或prestartAllCoreThreads方法动态覆盖,进行线程预启动。

keepAliveTime

keepAliveTime参数用来设置工作线程空闲时间。非核心线程通过poll方法获取任务,若获取不到,那么线程会keepAliveTime长的时间进行重复获取。当执行时间大于keepAliveTime时,线程就会采用CAS(比较-替换)随机方式进行线程销毁流程。

workQueue

workQueue参数用来指定存放提交任务的队列,任务BlockingQueue都可以用来传输和保存提交的任务。队列大小与线程数量之间存在关系:

a. 若线程数小于corePoolSize,对于提交的新任务会创建一个新的线程处理,不会把任务放入到队列。

b. 若线程数介于corePoolSize和maximumPoolSize之间,新提交的任务会被放到阻塞队列。

c. 若线程池处于饱和状态,即无法创建新线程且阻塞队列已满,那么新提交的任务交给拒绝策略处理。

线程池常见的阻塞队列一般有SynchronousQueue(同步队列)、LinkedBlockingQueue(无界阻塞队列)、ArrayLiBlockQueue(有界阻塞对列)、DelayedWorkQueue(延迟阻塞队列)。

  • SynchronousQueue不是真正意义上的队列,它没有容量也不存储任务,只是维护一组线程,等待着任务加入和移除队列,相当于直接交接任务给具体制定的队列,在CacheThreadPool线程池中使用。
  • LinkedBlockingQueue是采用链表实现的无界队列。若不预定义LinkedBlockingQueue队列容量,当所有核心线程都在执行时,就会无限添加任务到队列中,可能导致OOM,且这种场景下maximumPoolSize的值对线程数无影响。在SingleThreadExecutor和FixedThreadPool线程池中使用。
  • ArrayBlockingQueue是通过数组实现的有界队列。有界队列和有限的maximumPoolSize一起使用有助于防止资源耗尽。使用ArrayBlockingQueue可以根据应用场景,可以预先估计线程数和队列容量,互相权衡队列大小和线程数:
    1. 大队列和小线程数:减少线程数,可以最大限度的减少CPU使用率、操作系统资源和上下文切换开销,可能会导致吞吐量降低
    2. 小队列和大线程数:较大的线程,若任务提交速度快,会在短时间提升CPU使用率,提高系统吞吐量。若任务经常阻塞(如IO阻塞),会使得CPU切换更加频繁,可能会有更大的调度开销,这也会降低吞吐量
  • DelayedWorkQueue是采用数组的完全二叉树实现小根堆特性的延迟队列。在堆中每个父节点的值都不大于子节点的值,因此堆顶元素(数组第一个元素)总是当前队列中延时最小的任务。队列中元素是ScheduledFutureTask对象,对象封装了任务的执行逻辑以及延迟time信息。在ScheduledThreadPool线程池中使用。

threadFactory

该参数提供了线程池中线程创建方式,这里使用了工厂模式ThreadFactory创建新线程,默认情况下使用Executors.defaultThreadFactory创建,它创建的线程都在同一个ThreadGroup,且具有想同的NORM_PRIORITY优先级非守护进程状态。也可以自定义ThreadFactory修改线程的名称、线程组、优先级以及守护程序状态等。

handler

若线程池处于饱和状态没有足够的线程数或队列空间来处理提交的任务,或者线程池已处于关闭状态但还在处理进行中的任务,那么新提交的任务就会由拒绝策略处理。

出现以上任何情况,execute方法都会调用RejectExecutionHandler.rejectExecution()方法进行拒绝策略处理。线程池提供了四种预定义的拒绝处理策略

ThreadPoolExecutor.AbortPolicy(默认策略):

        行为:当工作队列已满且无法再添加新任务时,直接抛出RejectExecutionException异常

        场景:适用于哪些不能容忍任务被丢弃或延迟执行的场景。因为会立即通知调用者任务被拒绝,从而可以采取相应的处理措施。如订单处理

ThreadPoolExecutor.DisCardPolicy:

        行为:当工作队列已满且无法再添加新任务时,直接丢弃新任务,不做任务处理。

        场景:适用于哪些任务可以被安全忽略的场景,或任务执行与否对系统整体影响不大的情况。如日志收集

ThreadPoolExecutor.DiscardOldestPolicy:

        行为:当工作队列已满且无法再添加新任务时,丢弃队列中最早的任务(即等待时间最长的任务),然后尝试重新提交当前任务。

        场景:适用于哪些任务可以相互替代,或较早的任务执行结果对当前系统状态影响不大的场景。如消息队列处理系统,若消息队列已满且新消息已来,可以采用该策略

ThreadPoolExecutor.CallerRunsPolicy:

        行为:当工作队列已满且无法再添加新任务时,由提交任务的线程来执行该任务,即任务在调用线程(提交任务的线程)的上下文中执行。

        场景:适用于需保证任务不被丢弃,且任务执行时间相对较短的场景。如在线视频处理

除了上述四种提供的拒绝策略外,还可通过实现RejectedExecutionHandler接口来自定义拒绝策略。

3、工作流程

了解了线程池中生命周期和核心参数,接下来了解下线程池整体工作流程,如图:

上图是一张线程池工作的精简图,线程池工作流程主要包含提交任务、创建工作线程并启动、获取任务并执行、销毁工作线程几部分。

1. 提交任务

当线程池通过execute提交任务时,线程池有三种处理情况,分别是创建工作线程执行该任务、将任务添加到阻塞队列、拒绝该任务。提交任务过程可以拆分一下几步:

a. 线程数小于corePoolSize时,通过addWorker创建新的核心线程处理该任务

b. 线程数等于corePoolSize且非空闲时,将任务添加到阻塞队列中。

c. 若添加成功,需二次验证线程池状态,若为非RUNNING状态,则需将该任务从队列中移除,然后拒绝该任务。 若为RUNNING状态且当前工作线程数为0,则需主动创建一个空任务的非核心线程来执行队列中该任务。

d. 若添加失败,则队列已满,创建新的临时线程(非核心线程)执行该任务。

e. 若创建临时线程(非核心线程)失败,则说明工作线程等于maximumPoolSize,只能拒绝该任务。

ThreadPoolExecutor.execute源码解析

//ThreadPoolExecutor.execute()方法执行任务
public void execute(Runnable command){
    if(command == null){
        throw new NullPointerExecption();
    }

    int c = ctl.get();
    //获取ctl低29位中的工作线程数与核心线程数比较,若小于核心线程数 直接创建worker对象执行任务
    if(workerCount(c) < corePoolSize){
        if(addWorker(command, true))
            return;
        c = ctl.get();

    }
    //当线程池为RUNNING状态且worker数量超过核心线程数,任务直接放入到阻塞队列中
    if(isRunning(c) && workerQueue.offer(command)){
        int recheck = ctl.get();
        
        //线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()拒绝操作。
        //这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。
        if(!isRunning(recheck) && remove(command)){
            拒绝处理任务
            reject(command);
        }else if(workerCountof(recheck) == 0){
            //当工作线程个数为0时,创建一个空任务的非核心工作线程执行队列中任务
            //这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0
            addWorker(null, false);
        }            
    }
    // 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
    // 这儿有3点需要注意:
    // 1. 线程池不是运行状态时,addWorker内部会判断线程池状态
    // 2. addWorker第2个参数表示是否创建核心线程
    // 3. addWorker返回false,则说明任务执行失败,需要执行reject拒绝操作
    else if(!addWorker(command, false))        
        reject(command);
        
}
2. 创建工作线程并启动

ThreadPoolExecutor线程池核心任务单元是Worker内部类实现的。创建工作线程首先就是创建Worker对象,Worker对象是通过核心addWorker方法创建工作线程,在通过Worker对象中的Thread对象启动线程。addWorker方法包含两部分操作。

a. 校验线程池状态以及工作线程个数

        i. 先判断线程池状态

        ii. 在判断工作线程个数

        iii. 通过CAS原子性对ctl属性低29位的值+1

b. 添加工作线程并启动工作线程

        i. 所有校验通过后,先new Worker对象,再将worker对象放大HashSet集合中

        ii. 然后拿到worker对象中的Thread对象执行.start方法启动工作线程,通过runWorker执行任务

addWorker方法执行源码解析

private boolean addWorker(runnable firstTask, boolean core){
    //外层for循环是在校验线程池状态
    //内层for循环是在校验工作线程个数

    //retry是给外层for添加一个标记,是为了方便在内层for循环跳出外层for循环
    retry:
    for(; ;){
        //获取存储线程池状态的ctl属性
        int c = ctl.get();
        //拿到ctl的高3位的值 即线程池状态
        int rs = runStateOf(c);

        //========================线程池状态判断=======================//

        //如果线程池状态是SHUTDOWN;并且此时阻塞队列中有任务 核心线程数为0,添加一个工作线程(非核心线程)去处理阻塞队列的任务

        //判断线程池状态是否大于等于SHUTDOWN,若满足可能为SHUTDOWN、STOP、TIDING、TERMINATED,说明线程池状态不是RUNNING(才能处理任务)
        if(rs >= SHUTDOWN &&

        //若三个条件都满足,就代表是要添加非核心线程去处理阻塞队列中的任务
        //若三个条件有一个不满足,返回false 配合!,表示不需要添加工作线程
        !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())){
            //不需要添加工作线程
            return false;
        }

        for(; ;){
            //========================工作线程个数判断=======================//

            //获取ctl低29位的值,代表当前工作线程的个数
            int wc = workerCountOf(c);

            //若工作线程个数大于第29位最大值,不可以再添加工作线程,返回false
            if(wc >= CAPACITY || 

            //基于core来判断添加的是否为核心工作线程
            //若是核心线程:基于corePoolSize来判断
            //若是非核心线程:基于maximumPoolSize来判断
            wc >= (core ? corePoolSize : maximumPoolSize))
                //代表不能添加,工作线程个数不满足要求
                return false;
            

            //若可以添加,直接对ctl的第29位采用CAS方式 直接+1。因为添加工作线程可能出现并发情况 使用CAS保证操作的原子性
            if(compareAndIncrementWorkerCount(c))
                //CAS成功后,直接退出外层for循环,代表可以执行添加工作线程的操作了
                break retry;

            //若CAS操作失败,重新获取一次ctl的值
            c = ctl.get();
            //判断重新获取的ctl中,线程池的状态跟之前的是否有区别
            //若状态不一致,说明有变化,重新去判断线程池的状态
            if(runStateOf(c) != rs)
                //跳出一次外出for循环
                continue retry;
        }
    }
    //========================添加工作线程以及启动工作线程=======================//

    //声明三个变量
    //工作线程启动了没 默认false
    boolean workerStarted = false;
    //工作线程添加了没 默认false
    boolean workerAdded = false;
    //工作线程 默认null
    Worker w = null;


    try{
        //构建工作线程,并将任务传递进去
        w = new Worker(firstTask);
        //获取Worker中的Thread对象
        final Thread t = w.thread;
        //判断Thread对象是否为null,在new Worker时 内部会通过给予的ThreadFactory去构建Thread对象交给Worker
        //一般若为null,代表ThreadFactory有问题
        if(t != null){
            //加锁,保证使用workers(HashSet集合 线程不安全)成员变量以及对largestPoolSize赋值时 保证线程安全
            final ReentrantLock mainLock = this.mainLock;
            //加锁原因是HashSet线程不安全 加锁
            main.lock();

            try{
                //再次获取线程池状态
                int rs = runStateOf(ctl.get());
                //再次判断线程池状态
                //若状态满足 rs < SHUTDOWN,说明线程池状态为RUNNING,状态正常可以添加工作线程 执行if代码块
                //若状态为SHUTDOWN,且firstTask任务为null,添加非核心工作线程处理阻塞队列中的任务
                if(rs < SHUTDOWN ||
                (rs == SHUTDOWN && firstTask == null)){
                    //到这可以添加工作线程了
                    //校验ThreadFactory创建线程后 线程是否已自动启动,若已启动,抛异常
                    if(t.isAlive())
                        throw new IllegalThreadStateExecption();

                    //private final HashSet<Worker> workers = new HashSet<Worker>();
                    //将new好的worker添加到HashSet中
                    workers.add(w);
                    //获取HashSet的size,拿到工作线程的个数
                    int s = workers.size();

                    //largestPoolSize-表示历史最大线程数记录
                    //若当前最大工作线程个数 大于历史最大线程个数记录,就赋值
                    if(s > largestPoolSize)
                        largestPoolSize = s;

                    //添加工作线程成功
                    workerAdded = true;
                }
            }finally{
                mainLock.unLock();
            }

            //工作线程添加成功后 启动工作线程
            if(workerAdded){
                //直接启动Worker中的线程
                t.start();
                //启动工作线程成功
                workerStarted = true;
            }
        }
    }finally{
        //做补偿的操作,若工作线程启动失败,将这个添加失败的工作线程处理掉
        if(!workerStarted)
            addWorkerFailed(w);
    }
    //返回工作线程是否启动成功
    return workerStarted;
}
3. 获取任务并执行

通过执行runWorker方法来获取任务并执行,runWorker方法执行主要有以下几步

a. 获取当前线程以及worker中封装的任务

b. 判断当前任务是否为null,若不为null,直接执行。若为null,重阻塞队列中获取任务

c. 判断线程池状态以及当前线程中断标记位,若线程池状态为STOP或当前线程中断标记位为false,则中端当前线程

d. 执行任务

runWorker方法源码解析

final void runWorker(Worker w) {
    //拿到当前工作线程(w对应的线程)
    Thread wt = Thread.currentThread();
    //拿到Worker对象中封装的的任务
    Runnable task = w.firstTask;
    //将worker的firstTask归位
    w.firstTask = null;
    // 将Worker的state状态归为0,代表可以被中断
    w.unlock(); // allow interrupts
    // 任务执行时,勾子函数中是否出现异常的标识,默认为true-出现异常
    boolean completedAbruptly = true;
    try {
        //获取任务的第一个方式,就是执行execute、submit时,转入的任务直接处理
        //获取任务的第二个方式,从工作队列中获取任务执行。
        while (task != null || (task = getTask()) != null) {

            // 加锁,在SHUTDOWN状态下,当前线程不允许被中断
            // 并且Worker内部的锁,并不是可重入锁,因为中断时,也需要对worker进行lock,不能获取就代表当前线程正在执行任务
            w.lock();
            
            //如果线程池状态变为了STOP状态,必须将当前线程中断
            // 第一个判断:判断当前线程池状态是否为STOP
            // 第二个判断:查看线程中断标记位并归位,若为false 说明不是STOP,若变为true,需要再次查看是否是否是并发操作导致线程池为STOP
            if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
                // 查看当前线程中断标记是否为false,若为false,就执行wt.interrupt();
                !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
                task = null;
                // 将执行成功的任务个数+1 
                w.completedTasks++;
                // 将线程的state状态标记位设置为0,表示可以通过SHUTDOWN中断当前线程
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 自旋操作被退出,说明线程池正在结束
        processWorkerExit(w, completedAbruptly);
    }
}

三、总结

Executor框架主要由三部分组成,任务任务的执行者执行结果,ThreadPoolExecutor和ScheduledThreadPoolExecutor的设计思想也是将这三个关键要素进行了解耦,将任务的提交和执行分离。线程池是一种基于池化技术的线程管理工具,能够降低资源消耗、提高响应速度、提高线程的可管理性。

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

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

相关文章

万字详解 MapStruct Plus,带你快速上手!

与其明天开始&#xff0c;不如现在行动&#xff01; 文章目录 前言一、为什么要用 MapStruct&#xff08;背景&#xff09;二、MapStruct Plus 的快速开始1. 引入依赖2. 指定对象映射关系3. 编写测试代码4. 运行结果5. 原理解析 三、自定义实体类中的属性转换1. 自定义一个类型…

【IO】使用父子进程完成两个文件的拷贝,父进程拷贝前一半内容,子进程拷贝后一半内容,子进程结束后退出,父进程回收子进程的资源

1、使用父子进程完成两个文件的拷贝&#xff0c;父进程拷贝前一半内容&#xff0c;子进程拷贝后一半内容&#xff0c;子进程结束后退出&#xff0c;父进程回收子进程的资源 #include <myhead.h>int main(int argc, const char *argv[]) {//判断输入的格式是否符合要求i…

预测性维护:一种基于因果技术语言处理 (CTLP) 的智能故障诊断方法

关键词&#xff1a;预测性维护、因果贝叶斯网络、ROX数据 在工业运营和维护领域&#xff0c;资产绩效最大化和风险最小化至关重要。随着工业设备组件的磨损和恶化&#xff0c;系统会表现出一系列变化&#xff0c;这些变化的严重程度会逐渐增加&#xff0c;直到最终发生故障。在…

C++_string_知识总结(初学)

基础认识&#xff1a; string严格意义上不属于STL容器&#xff0c;其出现的时间早于STL&#xff0c;这也导致了string官方库中部分函数冗余。但是这也体现了语言发展中的一个重要现象——向前兼容。和很多STL容器一样&#xff0c;string是一个类&#xff0c;核心是一个顺序表&…

MySQL:GROUP BY 分组查询

分组查询是SQL中一个非常强大的功能&#xff0c;它允许我们将数据按照一个或多个字段进行分组&#xff0c;并对每个分组进行聚合计算&#xff08;如求和、平均值、最大值、最小值等&#xff09;。在MySQL中&#xff0c;我们使用 GROUP BY 关键字来实现分组查询。 核心语法 SE…

笑出腹肌的饼图绘制秘籍:Matplotlib让你秒变数据烘焙大师!

1. 引言 亲们&#xff0c;还在为数据报告里的饼图头疼吗&#xff1f;别怕&#xff0c;Matplotlib来救场啦&#xff01;它不只是个绘图工具&#xff0c;简直是数据界的魔术师&#xff0c;让你的饼图既专业又有趣。跟我学几招&#xff0c;保证让你的观众边吃边看&#xff0c;爱不…

Linux驱动开发—编写第一个最简单的驱动模块

文章目录 开发驱动准备工作1.正常运行的Linux系统的开发板2.内核源码树3.nfs挂载的rootfs4.得心趁手的IDE 第一个Hello world 驱动程序常见模块的操作命令模块的初始化和清理模块的版本信息模块中的各种宏 示例Hello World代码printk函数解析 使用MakeFile编译驱动模块使用insm…

谷歌账号异常,成功通过验证后这个界面操作指引:建议增加辅助手机和邮箱

许多朋友对下面这个界面都很熟悉&#xff0c;通常是账号被停用后的时候输入账号和密码后&#xff0c;还需要再次输入手机号码验证。而且这个时候输入国内的号码或者谷歌账号绑定的辅助手机号码都不管用&#xff0c;提示此电话号码用于验证的次数过多&#xff0c;或者此电话号码…

链表篇:03-合并有序链表

解题思路&#xff1a; 使用双指针&#xff0c;一个指针指向头节点&#xff0c;然后另外一个指针进行移动。让其头节点保持不动&#xff0c;最后循环遍历两个链表&#xff0c;将其挂到头指针所在的节点上。 temp 守卫节点&#xff0c;用于指向头节点&#xff0c;防止头节点丢…

机械学习—零基础学习日志(高数17——极限局部有界性)

零基础为了学人工智能&#xff0c;真的开始复习高数 这里我们更加详细讲解函数极限性质。上一篇文章里有一些内容还需要进一步补充。 局部有界性 这里是局部有界性的需要注意的事项。第3点&#xff0c;如果函数在闭区间内连续&#xff0c;则必定有界。试想一下&#xff0c;如…

Log4j2漏洞

Log4j2漏洞 步骤一:执行以下命令启动靶场环境并在浏览器访问!!! systemctl start docker cd vulhub/log4j/CVE-2021-44228 vi docker-compose.yml //编写docker-compose.xml的端口和版本号 docker-compose up -d # 访问网址 http://192.168.30.131:8983/solr/#/步骤二:先在自…

MyBatis入门如何使用操作数据库及常见错误(yml配置)

一&#xff0c;什么是MyBatis 是一款优秀的持久层框架&#xff0c;用于简化jdbc的开发 持久层&#xff1a;指的就是持久化操作的层&#xff0c;通常也就是数据访问层&#xff08;dao&#xff09;&#xff0c;也就是用来操作数据库。 也就是MyBatis是让你更加简单完成程序与数…

ECCV 2024前沿科技速递:GLARE-基于生成潜在特征的码本检索点亮低光世界,低光环境也能拍出明亮大片!

在计算机视觉与图像处理领域&#xff0c;低光照条件下的图像增强一直是一个极具挑战性的难题。暗淡的光线不仅限制了图像的细节表现&#xff0c;还常常引入噪声和失真&#xff0c;极大地影响了图像的质量和可用性。然而&#xff0c;随着ECCV 2024&#xff08;欧洲计算机视觉会议…

form表单按钮根据编辑/只读状态显示和隐藏

1. 场景阐述: form表单自定义按钮,在编辑模式显示,在只读模式隐藏 2. 效果: 这里的保存按钮是自定义按钮,在编辑状态的时候显示,非编辑状态下隐藏 3. 解决方案: 如下所示,只需要在按钮中添加odoo自带class类oe_edit_only即可 <header><button type"object"…

桌面管理利器:2024年度待办事项工具评选

国内外主流的10款待办事项桌面工具对比&#xff1a;PingCode、Worktile、滴答清单、番茄ToDo、Teambition、Tower、有道云笔记、TickTick、Any.do、Trello。 在忙碌的工作日中&#xff0c;管理日常任务和待办事项常常让人感到不胜其烦。选择合适的待办事项桌面工具&#xff0c;…

【Python实战因果推断】67_图因果模型2

目录 Are Consultants Worth It? Crash Course in Graphical Models Chains Are Consultants Worth It? 为了展示有向无环图(DAG)的力量&#xff0c;让我们考虑一个更有趣但处理因素并未随机化的情况。假设你是某公司的经理&#xff0c;正在考虑是否聘请顶级咨询顾问。你…

[数据结构] AVL树 模拟实现AVL树

标题&#xff1a;[数据结构] AVL树 && 模拟实现AVL树 水墨不写bug 正文开始&#xff1a; 目录 &#xff08;一&#xff09;普通二叉搜索树的痛点 &#xff08;二&#xff09;AVL树简介 &#xff08;1&#xff09;AVL树的概念 &#xff08;三&#xff09;AVL树的…

LeetCode面试150——189轮转数组

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及程序实现 3.1 暴力求解 3.2 循环链表 3.3 环状替代 3.4 数组翻转 4 题目难度 参考文献 1 题目描述 给定一个整数数组 nu…

运维.Linux.bash学习笔记.数组及其使用

运维专题 Bash Shell数组及其使用 此笔记当前仍在修改和编写。 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:http…

基于N32L406+Freertos+letter_shell终端开源库移植

移植教程 这里首先感谢作者的开源 https://gitee.com/zhang-ge/letter-shell) [Letter shell 3.0 全新出发 | Letter (nevermindzzt.github.io)](https://nevermindzzt.github.io/2020/01/19/Letter shell 3.0全新出发/) 1.复制代码 将litter_shell文件夹中的所有文件复制到…