当前位置: 首页 > news >正文

ThreadPoolExecutor 学习

ThreadPoolExecutor 是开发中最常用的线程池,今天来简单学习一下它的用法以及内部构造。

1、线程池存在的意义?

一般在jvm上,用户线程和操作系统内核线程是1:1的关系,也就是说,每次创建、销毁线程的时候,都会进行内核调用,产生比较大的开销,有了线程池,就可以重复的利用线程资源,大幅度降低创建和回收的效率。

此外线程池还可以帮助我们维护线程ID,线程状态等信息。

2、ThreadPoolExecutor的作用?

开发者将任务提交给ThreadPoolExecutor,ThreadPoolExecutor负责分配工作线程(Worker)来执行任务,任务完成后,工作线程不进行回收,而是继续等待后续任务。

3、如何使用ThreadPoolExecutor?

直接new一个ThreadPoolExecutor,使用的时候,调用其excute()方法,传入一个任务即可。

ThreadPoolExecutor包含了7个入参:

  1. * corePoolSize : 核心线程数
  2. * maximumPoolSize :线程池中允许的最大线程数
  3. * keepAliveTime:线程数目大于核心线程数时,多余空闲线程在终止前等待新任务的最长时间
  4. * unit:保存时间的单位
  5. * workQueue :用于任务执行前保存任务的队列
  6. * threadFactory :执行器创建新线程的工厂 Executors.defaultThreadFactory() 默认工厂
  7. * handler :拒绝策略,由于达到线程边界和队列容量而阻止执行时使用的处理程序 new ThreadPoolExecutor.AbortPolicy()抛出异常
public class TestThreadPool {public static void main(String[] args) {/*** 共有七个参数* corePoolSize : 核心线程数* maximumPoolSize :线程池中允许的最大线程数* keepAliveTime:线程数目大于核心线程数时,多余空闲线程在终止前等待新任务的最长时间* unit:保存时间的单位* workQueue :用于任务执行前保存任务的队列* threadFactory :执行器创建新线程的工厂  Executors.defaultThreadFactory() 默认工厂* handler :由于达到线程边界和队列容量而阻止执行时使用的处理程序 new ThreadPoolExecutor.AbortPolicy()抛出异常*/ExecutorService executorService = new ThreadPoolExecutor(3, 5, 1L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());for (int i = 0; i < 7; i++) {int finalI = i;executorService.execute(() -> {System.out.println(Thread.currentThread().getName() + "===>办理业务" + finalI);});}executorService.shutdown();}
}


学会了如何使用,接下来看看ThreadPoolExecutor的源码吧 


1、ThreadPoolExecutor定义的一些属性

除了构造器中需要传入的7个参数外,还要重点关注一下以下属性

  • ctl:原子类int, ThreadPoolExecutor用这样一个32位的int类型维护了两个核心属性
    • 线程池状态:高三位表示
    • 工作线程的个数:低29位表示,因此一个ThreadPoolExecutor最多运行2^29个工作线程
  • workers:存放工作线程的集合,是一个HashSet,因此在添加工作线程到workers里面的时候,要加锁保证线程安全
  • mainLock:一个ReentrantLock可重入锁
    /*** ctl基于一个int类型,维护了线程池的两个核心属性:* 1 线程池的状态: 高三位* 2 工作线程的个数:低二十九位 --> 因此,线程池中最多允许 2^29 个工作线程个数*/private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));/*** 下面的参数,就是为了方便运算定义的常量*/// Integer.SIZE=32, 因此COUNT_BITS=29private static final int COUNT_BITS = Integer.SIZE - 3;// 左移29位减一,也就是2^29private static final int CAPACITY   = (1 << COUNT_BITS) - 1;// 默认状态,可以正常接收执行处理任务private static final int RUNNING    = -1 << COUNT_BITS;// 执行shutdown()方法可以变成SHUTDOWN状态 ,优雅的关闭线程池// 不接受新任务,但是可以处理完已经提交的任务private static final int SHUTDOWN   =  0 << COUNT_BITS; // 执行shutdownNow()方法变成stop状态// 不接受新任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程 中断标志位private static final int STOP       =  1 << COUNT_BITS;// 所有任务执行完毕,池子中的工作线程数为0,等待执行 terminated()钩子方法private static final int TIDYING    =  2 << COUNT_BITS;// terminated()钩子方法,继承的时候可以实现一些自己的业务,执行完毕private static final int TERMINATED =  3 << COUNT_BITS;

线程池状态之间的转换关系图如下:

 2、内部类Worker

这里只介绍一个主要的内部类Worker,这个就是工作线程

  • 继承AQS:说明内部存在同步的需求(为了使用AQS中的state状态?)
  • 实现了Runnable接口:说明worker本身就是一个异步的任务调度者

当某个Worker获取到一个任务时,便持有锁,直到将任务在当前线程内执行完成后,再释放锁,然后在获取新的任务。

其实这里加锁其实分成了两个部分

  1. Worker初始化的时候,进行了一次加锁。state状态初始化为-1,然后在初始化成功后,去执行任务之前,将state置为0,
  2. 真正开始执行任务时,先进行一次加锁,执行完任务的时候,进行解锁。
// Worker构造器
Worker(Runnable firstTask) {// AQS的方法,将state设置为-1setState(-1); // inhibit interrupts until runWorker// 设置任务,并使用 用户传入的线程工厂 创建线程this.firstTask = firstTask;this.thread = getThreadFactory().newThread(this);
}
2.1:为什么要将state的状态设置为-1?

答:就是为了保证worker在“从初始化到开始执行任务”这个期间不接受中断信号,以保证当前的worker能够正常初始化完成。

详细解释:

1、首先,看一下 Worker 内部类中定义了一个interruptIfStarted方法:如果state为-1,就不允许接收中断信号

void interruptIfStarted() {Thread t;if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {try {t.interrupt();} catch (SecurityException ignore) {}}
}

2、worker开始执行任务的时候,也就是调用run方法的时候,会把state重新设置为0,此时,就可以接收中断信号了,看代码。

2.2 线程池如何执行任务,如何拉取工作队列中的任务?

主要代码逻辑在runWorker方法中,工作线程worker开始执行任务的时候,会调用自己的run方法,而runWorker的入参,就是worker自身。

  1. 执行初始化的任务:worker对象初始化的时候,是携带了任务的,那么就优先执行自身携带的任务 (每次执行任务前先判断线程池状态)
  2. 执行从工作队列中拉取的任务:从工作队列中拉取任务是通过getTask方法,这个方法里面是一个死循环
    1. 首先还是判断线程池状态
    2. 如果当前是核心线程,那就执行take方法,这个方法就是一直等待从工作队列中获取任务,直到成功或者当前线程中断
    3. 如果不是核心线程,那就执行poll方法,在指定时间内从工作队列中拉取任务,超时就退出 --- 初始化线程池传递的keepAliveTime
  3. 如果当前没有任务,就要将工作线程关闭了,getTask方法里面通过CAS修改了线程池的工作线程数目。---- 一个线程的关闭就是在run方法结束之后,结合2中介绍,如果是核心线程,就会一直等待下去,非核心线程在超时之后就会执行完runWorker方法,然后关闭该工作线程。
// runWorker方法,传入的是一个Worker对象
final void runWorker(Worker w) {// 获取当前线程,并取出第一个任务Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;w.unlock(); // allow interruptsboolean completedAbruptly = true;try {// Worker对象启动的时候,是携带了任务的,优先执行携带的任务// 第一个循环结束,task = null, 然后就通过getTask(),从工作队列中拉取任务while (task != null || (task = getTask()) != null) {w.lock();// 判断当前线程池状态是否是stop,如果是,就强制中断当前线程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);}
}// 从工作队列中拉取任务,是一个死循环
// 如果该线程是 核心线程,基于take方法从工作队列中拉取任务:死等下去,直到线程中断
// 如果非核心线程,基于poll方法,拉取指定时间的任务
private Runnable getTask() {boolean timedOut = false; // Did the last poll() time out?for (; ; ) {int c = ctl.get();int rs = runStateOf(c);// 还是先判断线程池状态,如果是stop 或者SHUTDOWN且工作队列中 没有任务了,就可以干掉当前任务if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {// CAS工作线程个数减一 // 结束一个线程就是run方法结束。// 外层是在while循环里面调用的run方法,这里取不到就不能继续进入循环, 上次循环的run方法执行完成,也就是说明当前工作线程结束了。decrementWorkerCount();return null;}int wc = workerCountOf(c);// Are workers subject to culling?boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;// 省略一堆判断try {// 这里的逻辑就是,如果是核心线程,就用take方法,如果不是核心线程,就用poll方法// poll方法:拉取阻塞队列中的任务,指定等待时间// take方法:死等下去,直到线程中断Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();if (r != null) {return r;}timedOut = true;} catch (InterruptedException retry) {timedOut = false;}}
}

3、线程池的主要执行方法excute():
 3.1 excute方法,主要是当用户提交一个任务给线程池之后,线程池的处理逻辑:
  1. 如果当前线程数小于核心线程数,就新创建一个worker执行任务
  2. 因为并发的原因,可能会创建失败,这时就尝试将任务放到任务队列中
  3. 如果此时工作队列也满了,加入失败,就尝试创建非核心工作线程处理任务
  4. 如果此时线程个数也已经达到了允许的最大线程数的限制,那么就执行拒绝策略。
/*** 任务交给线程池处理的时候,执行excute方法* 1、首先判断当前工作线程的个数是否小于定义的核心线程个数,如果小于,就创建核心工作线程执行当前任务,返回* 2、如果不小于核心工作线程个数,那么就尝试把任务放到工作队列中,如放入成功,就返回了* 3、如果工作队列已经满了,就判断当前工作线程数是否超过允许的最大线程数,如果小于,就创建非核心工作线程,执行该任务,返回* 4. 如果当前工作线程数目也等于允许的最大线程数了,就要执行拒绝策略了。* * * @param command 要传递的任务*/
public void execute(Runnable command) {// 非空校验if (command == null)throw new NullPointerException();// 拿到线程池的状态,以及工作线程的个数int c = ctl.get();// workerCountOf(c)就是从int类型的熟悉中,拿到工作线程的个数// 如果工作线程的个数小于核心线程数,那就创建工作线程if (workerCountOf(c) < corePoolSize) {// 创建工作线程,创建成功就返回if (addWorker(command, true))return;// 创建失败,可能是并发环境下有其他线程创建成功了,重新取一下ctlc = ctl.get();}// 核心线程数已经达到了预期的数量,就先尝试把任务放到工作队列中// 首先判断线程池状态是running,然后将任务放进队列中if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();// 任务入队前后线程池的状态可能会改变,如果此时不是running状态,就要对新加入的任务从队列中删除,并且执行拒绝策略if (! isRunning(recheck) && remove(command))reject(command);// 核心线程数可能设置为0,如果为0,就要创建非核心工作线程else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 任务放到队列中失败,尝试构建非核心线程去处理当前任务// 创建成功,就结束了else if (!addWorker(command, false))// 非核心线程创建失败,就执行拒绝策略。reject(command);
}

3.2 线程池中,创建worker的方法, addWorker:

addWorker方法就是用来创建线程,然后启动线程执行任务的。下面来看看他的具体逻辑:

  1. 首先是两层的for循环
    1. 外层for循环用于判断线程池的状态
      1. 如果不是running状态,那么就不能添加新任务了
      2. 此外经过一系列判断,确保在shutdown状态下,有工作线程能够处理阻塞队列中的任务(因此核心工作线程数目是可以为0的)
    2. 内层for循环用于判断线程池中工作线程的个数,
      1. 如果超过了要求的数目就返回
      2. 否则通过cas来使当前线程个数加1
  2. 然后就是创建worker,并执行任务
    1. 创建出来的worker要加入 HashSet类型的 worker中,因此要加锁处理
    2. 添加成功就执行该任务,并返回true,否则返回false
// 创建工作线程,包括核心线程和非核心线程
private boolean addWorker(Runnable firstTask, boolean core) {// 外层for循环判断线程池的状态// 内存for循环判断线程池的个数retry:for (; ; ) {int c = ctl.get();int rs = runStateOf(c); // rs就是高三位的线程池状态// rs >= SHUTDOWN 说明线程池不是running状态,不能接收新任务 -- 添加失败,返回false// SHUTDOWN可以正常处理工作队列的任务,后面的判断为了解决在SHUTDOWN状态下,没有工作线程处理工作队列中的任务的情况if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) {return false;}for (; ; ) {int wc = workerCountOf(c);// 工作线程数目大于等于最大数目2^29,或者大于等于 核心线程数/最大线程数 返回false,添加失败--- 根据是否创建核心线程确定的 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) {return false;}// cas修改工作线程数目,成功就跳出循环if (compareAndIncrementWorkerCount(c)) {break retry;}// CAS修改失败了,重新获取新的ctlc = ctl.get(); // 重新判断线程池状态,如果和原来不一样,就重新外层循环,因为外层循环是判断线程池状态的// 如果一样,说明线程池状态没变,继续内存循环就行了if (runStateOf(c) != rs) {continue retry;}}}// 定义两个标记,工作线程是否启动?工作线程是否创建?boolean workerStarted = false;boolean workerAdded = false;// w就是要创建的工作线程Worker w = null;try {/*** 创建工作线程,并且把任务交给这个线程new里面调用了线程工厂*         Worker(Runnable firstTask) {*             setState(-1); // inhibit interrupts until runWorker*             this.firstTask = firstTask;*             this.thread = getThreadFactory().newThread(this);*         }*/w = new Worker(firstTask);// 获取new worker时得到的线程对象final Thread t = w.thread;// 因为线程工厂是用户传进来的,所以thread可能为null,这里判断一下,增加代码的健壮性if (t != null) {// 这里用锁,对workers进行操作,将创建出来的工作线程加到workers 中// 这个workers是一个hashSetfinal ReentrantLock mainLock = this.mainLock;mainLock.lock();try {int rs = runStateOf(ctl.get());if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) {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;
}

4 线程池的关闭流程
4.1  shutdown() 方法

这个方法被称为温柔的终止线程池,不接受新任务,但是会处理完正在运行的任务和阻塞队列中的任务。--- 这个方法会将线程池状态修改为 SHUTDOWN

怎么判断哪些任务是空闲的,哪些任务是正在运行的呢?其实还是根据worker中的state的值来判断的,在循环workers的过程中,会尝试通过CAS将当前worker的状态从0修改到1,只有空闲状态状态的工作线程的state为0,修改成功然后执行interrupt命令,工作状态则不会。

// 温柔的终止线程池,不接受新任务,但是会处理完正在运行的和阻塞队列中的任务
public void shutdown() {final ReentrantLock mainLock = this.mainLock;// 可重入锁加锁,因为要操作workers了,这是一个hashsetmainLock.lock();try {// 权限校验checkShutdownAccess();// 将线程池状态设置为shutdownadvanceRunState(SHUTDOWN);// 这个方法里面中断所有的空闲线程interruptIdleWorkers();onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();}// 尝试终止线程池tryTerminate();
}// 中断所有的空闲线程
private void interruptIdleWorkers(boolean onlyOne) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {for (ThreadPoolExecutor.Worker w : workers) {Thread t = w.thread;// w.tryLock() ---  这里最终会调用CAS尝试把state从0修改到1// 但是我们知道工作中的线程是加了锁的,state的值不为0,因此工作线程CAS失败,不会进入判断// 只有空闲线程才会修改成功,然后执行interrupt方法if (!t.isInterrupted() && w.tryLock()) {try {t.interrupt();} catch (SecurityException ignore) {} finally {// tryLock里面cas成功了,state要减一w.unlock();}}// onlyOne如果为true,最多interrupt一个worker// 只有当终止流程已经开始,但线程池还有worker线程时,tryTerminate()方法会做调用onlyOne为true的调用// 在这种情况下,最多有一个worker被中断,为了传播shutdown信号,以免所有的线程都在等待// 为保证线程池最终能终止,这个操作总是中断一个空闲workerif (onlyOne) {break;}}} finally {mainLock.unlock();}
}

4.2  shutdownNow()方法

shutdownNow方法就很暴力,直接中断所有线程,即便当前线程正在执行任务,也会执行interrupt方法;然后将工作队列中的任务返回。

// 中断所有线程
public List<Runnable> shutdownNow() {List<Runnable> tasks;final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// 权限校验checkShutdownAccess();// 修改状态为stopadvanceRunState(STOP);// 简单粗暴,终止所有线程interruptWorkers();// 将工作线程中的任务都放到一个list中返回tasks = drainQueue();} finally {mainLock.unlock();}tryTerminate();return tasks;
}// 简单粗暴,除了初始化状态的线程,全部中断
private void interruptWorkers() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {for (ThreadPoolExecutor.Worker w : workers)w.interruptIfStarted();} finally {mainLock.unlock();}
}

4.3  tryTerminate() 方法

我们看到在shutdown和shutdownNow方法中都调用了tryTerminate() 方法(事实上,所有可能导致线程池产终止的地方都调用了tryTerminate() 方法),这个方法中,在工作线程不为0的时候,会去中断线程池中的一个空闲的线程,这样做的目的是:当满足终结线程池的条件但是工作线程数不为0, 这个时候需要中断一个空闲的工作线程去确保线程池关闭的信号得以传播

想象这样一种场景:调用shutdown时,多个worker正在运行,且此时工作队列也不为空,当所有的任务都执行完毕时,核心线程会被 queue.take()阻塞,无法终止线程,但是因为调用了showdown,后续也无法接收新任务了,这是不合理的,因此需要在showdown之后还可以发出中断信号。事实上,所有可能导致线程池产终止的地方都调用了tryTerminate() 方法,如果线程池进入了终止流程,但是还有空闲线程,就中断一个空闲线程。

// 每个工作线程结束的时候都会调用tryTerminate方法
final void tryTerminate() {for (; ; ) {int c = ctl.get();// 还是判断线程池的状态if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty())) {return;}// 工作线程不为0,则会中断工作线程集合中的第一个空闲的线程// ONLY_ONE为 true表示只中断一个线程,会在遍历的时候跳出循环// 当满足终结线程池的条件但是工作线程数不为0,这个时候需要中断一个空闲的工作线程去确保线程池关闭的信号得以传播。if (workerCountOf(c) != 0) { // Eligible to terminateinterruptIdleWorkers(ONLY_ONE);return;}final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// CAS设置线程池状态为TIDYING,if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {try {terminated();} finally {// 最后更新线程池状态为TERMINATEDctl.set(ctlOf(TERMINATED, 0));termination.signalAll();}return;}} finally {mainLock.unlock();}// else retry on failed CAS}
}

介绍一下Thread中的几个方法。

  • stop()方法:立即停止当前线程,并释放当前线程的锁,已经弃用了。
  • interrupt()方法:给当前线程设置一个中断标志位,当前任务还会继续执行,
  • isInterrupted()方法:返回当前线程的中断标志位。
  • interrupted()方法:这个是一个静态方法,也是查询当前线程的中断标志位,但是查询之后会把当前线程的中断标记位清除。

相关文章:

ThreadPoolExecutor 学习

ThreadPoolExecutor 是开发中最常用的线程池&#xff0c;今天来简单学习一下它的用法以及内部构造。 1、线程池存在的意义&#xff1f; 一般在jvm上&#xff0c;用户线程和操作系统内核线程是1&#xff1a;1的关系&#xff0c;也就是说&#xff0c;每次创建、销毁线程的时候&am…...

深入理解计算机操作系统书籍阅读感悟(一)

1.sp&#xff1a;表示为空格&#xff0c;ASCII为32 2.在我们写的每行程序结尾都有一个隐藏的\n&#xff08;ASCII码值为10&#xff09; 3.在书上的P2页上说&#xff1a;文本文件是指以ASCII码字符构成的文件&#xff0c;其余都是二进制文件 除了这种理解&#xff0c;更常见的…...

使用query请求数据出现500的报错

我在写项目的时候遇到了一个问题&#xff0c;就是在存商品id的时候我将它使用了JSON.stringify的格式转换了&#xff01;&#xff01;&#xff01;于是便爆出了500这个错误&#xff01;&#xff01;&#xff01; 我将JSON.stringify的格式去除之后&#xff0c;它就正常显示了&…...

PostgreSQL教程(二十一):服务器管理(三)之服务器设置和操作

本章讨论如何设置和运行数据库服务器&#xff0c;以及它与操作系统的交互。 一、PostgreSQL用户账户 和对外部世界可访问的任何服务器守护进程一样&#xff0c;我们也建议在一个独立的用户账户下运行PostgreSQL。这个用户账户应该只拥有被该服务器管理的数据&#xff0c;并且…...

Linux运维_Bash脚本_编译安装GNU-Tools

Linux运维_Bash脚本_编译安装GNU-Tools Bash (Bourne Again Shell) 是一个解释器&#xff0c;负责处理 Unix 系统命令行上的命令。它是由 Brian Fox 编写的免费软件&#xff0c;并于 1989 年发布的免费软件&#xff0c;作为 Sh (Bourne Shell) 的替代品。 您可以在 Linux 和 …...

leetcode 121.买卖股票的最佳时机

声明&#xff1a;以下仅代表个人想法&#xff0c;非官方答案或最优题解&#xff01; 题目&#xff1a; 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的…...

javaWebssh酒店客房管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh酒店客房管理系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0…...

vue3基础教程(2)——创建vue3+vite项目

博主个人微信小程序已经上线&#xff1a;【中二少年工具箱】。欢迎搜索试用 正文开始 专栏简介1. 前言2.node版本检测3.创建vue项目 专栏简介 本系列文章由浅入深&#xff0c;从基础知识到实战开发&#xff0c;非常适合入门同学。 零基础读者也能成功由本系列文章入门&#x…...

部署DNS 实战篇

二、DNS 部署 环境介绍 服务器3台、系统centos 安装软件 yum install -y bind bind-utils bind-chrootbind 主包bind-utils 客户端测试工具&#xff08;host 、dig 、nslookup&#xff09;bind-chroot chroot环境 禁锢dns服务器的工作目录caching-nameserver(rhel5提供…...

2023 2024年全国职业院校技能大赛中职组网络建设与运维赛项服务器Linux部分教程解析

欢迎合作 需要资料请私 Rocky 9 包含各种常考服务(包括新题型KVM等)...

Flask g对象和插件

四、Flask进阶 1. Flask插件 I. flask-caching 安装 pip install flask-caching初始化 from flask_cache import Cache cache Cache(config(CACHE_TYPE:"simple" )) cache.init_app(appapp)使用 在视图函数上添加缓存 blue.route("/") cache.cached(tim…...

26、Qt调用.py文件中的函数

一、开发环境 Qt5.12.0 Python3.7.8 64bit 二、使用 新建一个Qt项目&#xff0c;右击项目名称&#xff0c;选择“添加库” 选择“外部库”&#xff0c;点击“下一步” 点击“浏览”&#xff0c;选择Python安装目录下的libs文件夹中的“python37.lib”文件&#xff0c;点击“下…...

计算机网络实验一 网线制作

实验目的与要求&#xff1a; 实验目的 了解以太网网线&#xff08;双绞线&#xff09;和制作方法 实验内容 了解网线和水晶头 学习网线制作方法 实验环境和要求 网线 水晶头 压线钳 剥线钳 网线测试器 方法、步骤&#xff1a; 步骤一 准备工具和材料 步骤二 剥掉双绞线的外…...

android TextView 实现富文本显示

android TextView 实现富文本显示&#xff0c;实现抖音直播间公屏消息案例 使用&#xff1a; val tvContent: TextView helper.getView(R.id.tvContent)//自己根据UI业务要求&#xff0c;可以控制 图标显示 大小val levelLabel MyImgLabel( bitmap 自己业务上的bitmap )va…...

Linux常用命令(超详细)

一、基本命令 1.1 关机和重启 关机 shutdown -h now 立刻关机 shutdown -h 5 5分钟后关机 poweroff 立刻关机 重启 shutdown -r now 立刻重启 shutdown -r 5 5分钟后重启 reboot 立刻重启 1.2 帮助命令 –help命令 shutdown --help&#xff1a; ifconfig --help&#xff1a;查看…...

软考笔记--基于架构的软件开发方法

一.体系架构的设计方法概述 基于体系结构的软件设计方法ABSD是由体系结构驱动的&#xff0c;即指有构成体系结构的商业、质量和功能需求的组合驱动的。ABSD方法有3个基础。第1个基础是功能的分解。在功能分解中&#xff0c;ABSD方法使用已有的基于模块的内聚和耦合技术。第2个…...

CSS 盒子模型(box model)

概念 所有HTML元素可以看作盒子&#xff0c;在CSS中&#xff0c;"box model"这一术语是用来设计和布局时使用CSS盒模型本质上是一个盒子&#xff0c;封装周围的HTML元素&#xff0c;它包括&#xff1a;外边距(margin)&#xff0c;边框(border)&#xff0c;内边距(pad…...

基于springboot+vue的在线考试系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…...

001 概述

什么是API API&#xff08;Application Programming Interface,应用程序编程接口&#xff09;是一些预先定义的函数&#xff0c;目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力&#xff0c;而又无需访问源码&#xff0c;或理解内部工作机制的细节。为了…...

linux环境下nginx的配置文件

根据指定的域名进行反向代理转发&#xff0c;实现负载均衡 Nginx的upstream支持如下六种方式的分配算法&#xff0c;分别是&#xff1a; 轮询 默认方式 weight 权重方式 ip_hash 依据ip分配方式 least_conn 依据最少连接方式 url_hash 依据URL分配方式 fair 依据响应时间…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

Qt Http Server模块功能及架构

Qt Http Server 是 Qt 6.0 中引入的一个新模块&#xff0c;它提供了一个轻量级的 HTTP 服务器实现&#xff0c;主要用于构建基于 HTTP 的应用程序和服务。 功能介绍&#xff1a; 主要功能 HTTP服务器功能&#xff1a; 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中&#xff0c;将 long long 类型转换为 QString 可以通过以下两种常用方法实现&#xff1a; 方法 1&#xff1a;使用 QString::number() 直接调用 QString 的静态方法 number()&#xff0c;将数值转换为字符串&#xff1a; long long value 1234567890123456789LL; …...

DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”

目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

智能AI电话机器人系统的识别能力现状与发展水平

一、引言 随着人工智能技术的飞速发展&#xff0c;AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术&#xff0c;在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发&#xff0c;实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构&#xff0c;服务器端使用Java Servlet处理请求&#xff0c;数据库采用MySQL存储信息&#xff0…...

基于SpringBoot在线拍卖系统的设计和实现

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...