万字详解,Java实现低配版线程池

news/2024/4/24 17:03:52/文章来源:https://blog.csdn.net/qq_62835094/article/details/136542000

文章目录

  • 1.什么是线程池
  • 2.线程池的优势
  • 3.原理
  • 4.代码编写
    • 4.1 阻塞队列
    • 4.2 ThreadPool线程池
    • 4.3 Worker工作线程
    • 4.4 代码测试
  • 5. 拒绝策略
    • 5.1 抽象Reject接口
    • 5.2 BlockingQueue新增tryPut方法
    • 5.3 修改ThreadPool的execute方法
    • 5.4 ThreadPool线程池构造函数修改
    • 5.5 拒绝策略实现
      • 1. 丢弃策略
      • 2. 移除最老元素
      • 3. 死等
      • 4. 抛出异常
    • 5.6 代码测试
  • 6.全部代码

1.什么是线程池

线程池,通过创建了一定数量的线程并将其维护在一个池(即容器)中。当有新的任务提交时,线程池会从池子中分配一个空闲线程来执行任务,而不是每次都新建线程。执行完任务后,线程不会被销毁,而是返回到线程池中等待下一个任务。

2.线程池的优势

  1. 避免线程的重复创建与销毁:对于需要执行的任务,如果每个任务都需要创建一个线程,线程执行完任务后销毁,那么会极大的造成资源的浪费。一方面任务数量可能会很庞大,创建与之匹配的线程会对内存造成严重消耗;另一方面,创建完的线程只工作一次,资本家看了落泪,md血亏啊
  2. 降低资源消耗:创建的线程反复利用,避免了创建与销毁带来的开销
  3. 提高工作的准备时间:被提交的任务可以迅速被线程池中存储的线程执行,无需重新创建

3.原理

线程池中存在以下核心组件

  • 线程池容器(存储工作线程)
  • 任务队列(存储需要执行的任务)

下述代码中,线程池使用HashSet存储;任务队列,使用的是这篇文章实现的BlockingQueue阻塞队列

另外,单纯的Thread线程能够存储的信息太少,因此我们创建Worker对象,extents Thread来包装Thread

下图是线程池的工作流程
在这里插入图片描述
大体来说,线程池执行逻辑分为三大步骤

  1. 如果current thread number < coreSize,创建核心线程执行任务

    tip:

    • current thread number在源码中,是有一个AtomicInteger变量ctl表示。ctl是核心线程池状态控制器,它被分为两个组成部分。其中,高三位表示runStatus,线程池状态;低三位表示workCount,工作线程数量。
    • 选择一个变量ctl同时存储runStatus和workCount,可以通过一次CAS操作实现原子赋值,而不用两次。
  2. 如果核心线程创建失败,或者核心线程数量过多,则将任务存储在阻塞队列中:在这一步中,存在非常多的细节。
    2.1. 如果当前线程池不处于RUNNING状态,尝试创建救急线程运行,不执行入队操作
    2.2. 如果入队失败,同样创建救急线程
    2.3. 如果线程池处于运行状态,且入队成功。进行double-check,重新检查线程池状态ctl
    2.4. 如果此时线程池不处于RUNNIG状态,移除刚入队的任务,并执行reject策略
    2.5. 如果线程池依然处于RUNNIG状态,且工作线程为0,创建救急线程,执行任务
  3. 如果上述步骤均失败,创建救济线程,如果依然失败,执行reject策略

4.代码编写

4.1 阻塞队列

实现请看BlockingQueue阻塞队列,本文不再赘述

4.2 ThreadPool线程池

/*** 线程池*/
@Slf4j
public class ThreadPool {// 核心线程数private int coreSize;// 阻塞队列private BlockingQueue<Runnable> workQueue;// 队列容量private int capacity;// 工作线程private final HashSet<Worker> workers = new HashSet<>();// todo: Worker(详见下一部分)private final class Worker extents Thread { /*...*/ }public ThreadPool(int coreSize, int capacity) {this.coreSize = coreSize;this.capacity = capacity;this.workQueue = new BlockingQueue<>(capacity);}/*** 执行task任务. 如果当前线程数量 < coreSize, 创建线程执行* 否则加入阻塞队列. 如果阻塞队列已满, 执行当前拒绝策略* @param task 需要执行任务*/public void execute(Runnable task) {if (task == null)throw new NullPointerException("task is null");synchronized (workers) {if (workers.size() < coreSize) {// 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)log.info("创建worker");Worker worker = new Worker(task);workers.add(worker);worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)}else {log.info("添加阻塞队列");// 添加阻塞队列workQueue.put(task);}}}
}

上述代码实现简易版线程池。

  • workQueue:阻塞队列,用于存储待执行的任务
  • coreSize:核心线程数量
  • capacity:阻塞队列大小
  • workers:工作线程的存储容器(线程池),用HashSet实现。请注意,HashSet是线程不安全的,因此在对HashSet操作时,记得加锁保证不会出现并发问题

本节对execute执行逻辑进行一定的简化,暂时不考虑拒绝策略(后续介绍)。

  • 如果当前线程数量 < coreSize,创建核心线程并执行任务
  • 否则添加阻塞队列

tip: 如果任务数量超过阻塞队列容量,那么依据阻塞队列的性质,后续的所有线程都会阻塞,等待容量减少。

4.3 Worker工作线程

我们使用包装过后的线程对象。且Worker是ThreadPool的内部类

private final class Worker extends Thread {// 执行的任务private Runnable task;Worker(Runnable task) {this.task = task;}/*** 执行task任务, 如果task为null, 则从workQueue工作队列中获取任务* 如果工作队列中不存在等待执行的任务, 终止当前Worker工作线程*/@Overridepublic void run() {while (task != null || (task = workQueue.take()) != null) {try {log.info("运行任务");task.run();} catch (Exception e) {e.printStackTrace();} finally {task = null;}}// 移除当前工作线程synchronized (workers) {workers.remove(this);}}
}

为了简化代码编写,本文只存在核心线程。核心线程的工作是监视阻塞队列,获取待执行的任务并执行

run方法中,while循环的条件有二

  • task != null: worker线程创建时,会分配第一个待执行的任务。如果待执行的任务不为null,则执行任务
  • task = workQueue.take():worker线程持续监视workQueue阻塞队列中的任务,如果存在任务,获取并执行

tip: workQueue.take()是一个阻塞的方法,没有时间的限制。也就是说,哪怕workQueue为空,该方法也会死等下去

4.4 代码测试

import lombok.extern.slf4j.Slf4j;@Slf4j
public class Test {public static void main(String[] args) throws InterruptedException {ThreadPool threadPool = new ThreadPool(2, 5);for (int i = 0; i < 10; i++) {int j = i;// 任务创建时间为2s, 任务消费时间显著低于任务创建时间.// 因此本模型是个典型的快生产, 慢消费的模型threadPool.execute(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info(String.valueOf(j));});}}
}

控制台输出

21:07:34.189 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:07:34.202 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:07:34.202 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.203 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:34.209 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.223 [Thread-0] INFO com.fgbg.juc.Test - 0
21:07:36.223 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:36.223 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.239 [Thread-1] INFO com.fgbg.juc.Test - 1
21:07:36.240 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.240 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:38.239 [Thread-0] INFO com.fgbg.juc.Test - 2
21:07:38.239 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:38.256 [Thread-1] INFO com.fgbg.juc.Test - 3
21:07:38.256 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:40.250 [Thread-0] INFO com.fgbg.juc.Test - 4
21:07:40.250 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:40.265 [Thread-1] INFO com.fgbg.juc.Test - 5
21:07:40.266 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:42.252 [Thread-0] INFO com.fgbg.juc.Test - 6
21:07:42.252 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:42.268 [Thread-1] INFO com.fgbg.juc.Test - 7
21:07:42.268 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:44.260 [Thread-0] INFO com.fgbg.juc.Test - 8
21:07:44.275 [Thread-1] INFO com.fgbg.juc.Test - 9

线程池核心线程数为2,因此一开始迅速创建2个worker线程。但因为阻塞队列容量为5,且每个线程工作需要2s,耗时远远小于任务产出的速度,因此队列被迅速沾满

当提交第8个任务时,主线程进入阻塞状态,无法继续提交任务(2个任务正在执行 + 5个任务添加阻塞队列 + 1个任务刚要入队,就阻塞了)

当第一个任务被执行完成,Thread-0 Worker执行阻塞队列中的其他任务。此时存在多余位置,之前被阻塞主线程成功提交任务,并继续循环

后续的流程大体一致,故不在做多余分析。

5. 拒绝策略

所谓拒绝策略,就是提供给调用方一个选择。如果调用方提交了过量的任务,多余的任务作何种处理。

由上方代码分析可知,我们一开始对于过量的任务,处理方案就是死等。但这种方案无法满足其他特定的需求,比如某个场景对执行速度有要求,等待一段时间后阻塞队列依然无法处理额外的任务,那么主线程就要抛弃该任务。死等是处理的方式之一,但存在不少的局限性,我们需要更多的处理方式。

对于不同的处理方式,我们可以选择将代码写死在ThreadPool中,但这样太不灵活,对于不同的场景,我们需要添加大量if else。因此我们可以采用策略模式,将拒绝的行为抽象成一个接口,创建ThreadPool时,由调用方传递接口。这样我们就可以在不改变ThreadPool内部代码的同时,改变ThreadPool面对超量任务的拒绝行为

5.1 抽象Reject接口

@FunctionalInterface
public interface RejectPolicy {// 执行拒绝策略void reject(Runnable task, BlockingQueue<Runnable> workQueue);
}

5.2 BlockingQueue新增tryPut方法

tryPut方法,尝试将元素立刻添加到阻塞队列中,不支持阻塞等待

// 尝试立即添加元素
public boolean tryPut(T task) {lock.lock();try {if (deque.size() == capacity) return false;deque.addLast(task);return true;} finally {lock.unlock();}
}

5.3 修改ThreadPool的execute方法

execute执行task入队操作时,如果入队失败(阻塞队列已满),则调用reject执行拒绝策略

    public void execute(Runnable task) {if (task == null)throw new NullPointerException("task is null");synchronized (workers) {if (workers.size() < coreSize) {// 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)log.info("创建worker");Worker worker = new Worker(task);workers.add(worker);worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)}else {log.info("添加阻塞队列");/*----------------modify below-------------------*/// 添加阻塞队列// workQueue.put(task);// 添加失败if ( !workQueue.tryPut(task)) {// 执行拒绝策略rejectPolicy.reject(task, workQueue);}}}}

5.4 ThreadPool线程池构造函数修改

    // 拒绝策略private RejectPolicy rejectPolicy;public ThreadPool(int coreSize, int capacity, RejectPolicy rejectPolicy) {this(coreSize, capacity);this.rejectPolicy = rejectPolicy;}

5.5 拒绝策略实现

因为RejectPolicy接口有@FunctionalInterface,支持lambda表达式,因此编写的时候可以简写

1. 丢弃策略

(task, workQueue) -> {}

2. 移除最老元素

(task, workQueue) -> { workQueue.poll(); }

tip: 笔者自定义的BlockingQueue没有实现poll方法,各位读者如果感兴趣,可以自行实现。需要注意的是,记得加锁保证线程安全

3. 死等

(task, workQueue) -> { workQueue.put(task); }

4. 抛出异常

(task, workQueue) -> { new RuntimeException("workQueue is full"); }

5.6 代码测试

@Slf4j
public class Test3 {public static void main(String[] args) throws InterruptedException {ThreadPool threadPool = new ThreadPool(2, 5, (task, workQueue) -> {log.info("任务丢弃");});for (int i = 0; i < 10; i++) {int j = i;threadPool.execute(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info(String.valueOf(j));});}}
}

上述代码选择的拒绝策略是丢弃

控制台输出

21:46:37.621 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:46:37.630 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.632 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.632 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.633 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:39.636 [Thread-1] INFO com.fgbg.juc.Test3 - 1
21:46:39.636 [Thread-0] INFO com.fgbg.juc.Test3 - 0
21:46:39.636 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:39.636 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:41.644 [Thread-0] INFO com.fgbg.juc.Test3 - 3
21:46:41.645 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:41.644 [Thread-1] INFO com.fgbg.juc.Test3 - 2
21:46:41.645 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:43.651 [Thread-1] INFO com.fgbg.juc.Test3 - 5
21:46:43.651 [Thread-0] INFO com.fgbg.juc.Test3 - 4
21:46:43.651 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:45.657 [Thread-1] INFO com.fgbg.juc.Test3 - 6

由日志可知,第8,9,10号任务被丢弃。任务对应的输出为7,8,9。观察输出的数字,发现最大值为6。因此确认了7~10号任务全部被拒绝,测试成功

6.全部代码

BlockingQueue


import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;// 消息队列(阻塞队列)
class BlockingQueue<T> {// 队列private Deque<T> deque = new ArrayDeque<>();// 容量private int capacity;// 锁private final ReentrantLock lock = new ReentrantLock();// 消费者等待条件private Condition consumerWaitSet = lock.newCondition();// 生产者等待条件private Condition producerWaitSet = lock.newCondition();public BlockingQueue(int capacity) {this.capacity = capacity;}// 添加元素public void put(T element) {lock.lock();try {// 队列已满while (deque.size() == capacity) {try {// 阻塞等待producerWaitSet.await();} catch (InterruptedException e) {}}// 添加元素deque.addLast(element);// 唤醒其它线程consumerWaitSet.signal();} finally {lock.unlock();}}// 获取元素public T take() {lock.lock();try {// 判空while (deque.size() == 0) {try {// 阻塞等待consumerWaitSet.await();} catch (InterruptedException e) {}}// 获取元素T res = deque.pollFirst();producerWaitSet.signal();return res;} finally {lock.unlock();}}// 尝试立即添加元素public boolean tryPut(T task) {lock.lock();try {if (deque.size() == capacity) return false;deque.addLast(task);return true;} finally {lock.unlock();}}
}

RejectPolicy

@FunctionalInterface
public interface RejectPolicy {// 执行拒绝策略void reject(Runnable task, BlockingQueue<Runnable> workQueue);
}

ThreadPool

import lombok.extern.slf4j.Slf4j;import java.util.HashSet;/*** 线程池*/
@Slf4j
public class ThreadPool {// 核心线程数private int coreSize;// 阻塞队列private BlockingQueue<Runnable> workQueue;// 队列容量private int capacity;// 工作线程private final HashSet<Worker> workers = new HashSet<>();// 拒绝策略private RejectPolicy rejectPolicy;private final class Worker extends Thread {// 执行的任务private Runnable task;Worker(Runnable task) {this.task = task;}/*** 执行task任务, 如果task为null, 则从workQueue工作队列中获取任务* 如果工作队列中不存在等待执行的任务, 终止当前Worker工作线程*/@Overridepublic void run() {while (task != null || (task = workQueue.take()) != null) {try {log.info("运行任务");task.run();} catch (Exception e) {e.printStackTrace();} finally {task = null;}}// 移除当前工作线程synchronized (workers) {workers.remove(this);}}}public ThreadPool(int coreSize, int capacity) {this.coreSize = coreSize;this.capacity = capacity;this.workQueue = new BlockingQueue<>(capacity);}public ThreadPool(int coreSize, int capacity, RejectPolicy rejectPolicy) {this(coreSize, capacity);this.rejectPolicy = rejectPolicy;}/*** 执行task任务. 如果当前线程数量 < coreSize, 创建线程执行* 否则加入阻塞队列. 如果阻塞队列已满, 执行当前拒绝策略* @param task 需要执行任务*/public void execute(Runnable task) {if (task == null)throw new NullPointerException("task is null");synchronized (workers) {if (workers.size() < coreSize) {// 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)log.info("创建worker");Worker worker = new Worker(task);workers.add(worker);worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)}else {log.info("添加阻塞队列");// 添加阻塞队列// workQueue.put(task);// 添加失败if ( !workQueue.tryPut(task)) {// 执行拒绝策略rejectPolicy.reject(task, workQueue);}}}}
}

Test

import lombok.extern.slf4j.Slf4j;@Slf4j
public class Test {public static void main(String[] args) throws InterruptedException {ThreadPool threadPool = new ThreadPool(2, 5);for (int i = 0; i < 10; i++) {int j = i;threadPool.execute(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info(String.valueOf(j));});}}
}

Test3

@Slf4j
public class Test3 {public static void main(String[] args) throws InterruptedException {ThreadPool threadPool = new ThreadPool(2, 5, (task, workQueue) -> {log.info("任务丢弃");});for (int i = 0; i < 10; i++) {int j = i;threadPool.execute(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.info(String.valueOf(j));});}}
}

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

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

相关文章

运输层端口号、复用和分用的概念

运行在计算机上的进程用进程标识符PID来标志&#xff0c;但不同操作系统使用的进程标识符又不一样。 所以为了能让不同计算机上的进程进行网络通信&#xff0c;就需要统一的方式来进行标识&#xff0c;这就是TCP/IP体系的端口号&#xff0c;范围是0~65535&#xff0c;2字节 运…

详解float函数类型转换

函数描述 float([x]) 函数将数字或数字的字符串表示形式转换为与它等效的有符号浮点数。如果参数x是一个字符串&#xff08;十进制表示的数字串&#xff09;&#xff0c;数字前面可以添加符号来表示正数&#xff0c;或负数。符号和数字之间不能出现空格&#xff0c;但是符号前…

【Web前端】Vue核心基础

文章目录 1. Vue简介2. Vue官网使用指南3. 初识Vue3.1 搭建Vue开发环境3.2 HelloWorld案例3.3 el与data的两种写法3.4 MVVM模型3.5 模板语法 4. 数据绑定4.1 v-bind单向数据绑定4.2 v-model双向数据绑定 5. 事件处理5.1 v-on绑定事件5.2 事件修饰符5.3 键盘事件 6. 计算属性6.1…

关于出国留学和考研比较----以本人双非跨考计算机为例

文章目录 中心论点国内就业现状勿让旧认知害了自己那出国留学真的一无是处了吗?1. 藤校仍旧是具有极高价值2. 时间成本低3. 研究生一定比单纯的本科找工作强!4. 很多人说出国读博好,可以无脑入,真是这样吗? 中心论点 如果在选择出国留学还是国内考研的最终核心诉求都是有更好…

【kubernetes】关于k8s集群的污点和容忍,以及k8s集群的故障排查思路

目录 一、污点 关于污点的增删改查 验证污点的作用——NoExecute ​编辑 验证污点的作用——NoSchedule 验证污点的作用——PreferNoSchedule 二、容忍 三、关于cordon 和 drain 四、Pod启动阶段 五、关于pod的五种状态 六、k8s常见的排障手段 针对组件故障 针对pod…

04 Python的面向对象编程

self是一个指针&#xff0c;指向对象xiaoming,可以调用对象里面的属性和方法。 name 和 age是实例变量。 desplay_msg是类变量 xiaoming就是具体的对象&#xff0c;是类的实例化

如何使用apk2url从APK中快速提取IP地址和URL节点

关于apk2url apk2url是一款功能强大的公开资源情报OSINT工具&#xff0c;该工具可以通过对APK文件执行反汇编和反编译&#xff0c;以从中快速提取出IP地址和URL节点&#xff0c;然后将结果过滤并存储到一个.txt输出文件中。 该工具本质上是一个Shell脚本&#xff0c;专为红队…

字节跳动发布SDXL-Lightning模型,支持WebUI与ComfyUI双平台,只需一步生成1024高清大图!

字节跳动发布SDXL-Lightning模型,WebUI与ComfyUI平台,只需一步生成1024高清大图,需要的步数比 LCM 更低了! 什么是SDXL-Lightning: SDXL-Lightning 是一种快速的文本到图像生成模型。SDXL-Lightning模型的核心优势在于其创新的蒸馏策略,它可以通过几个步骤生成高质量的 1…

Unity使用UnityWebRequest读取音频长度不对的解决方法

在开发的过程中碰到这样一个问题&#xff0c;有的音频文件通过UnityWebRequest读取出来后&#xff0c;AudioClip的Length会不对&#xff0c;比如本身有7秒&#xff0c;读出来只有3秒。代码如下&#xff1a; IEnumerator TestEnumerator() {UnityWebRequest www UnityWebReque…

利用“定时执行专家”循环执行BAT、VBS、Python脚本——含参数指定功能

目录 一、软件概述 二、VBS脚本执行设置 三、触发器设置 四、功能亮点 五、总结 在自动化办公和日常计算机任务管理中&#xff0c;定时执行脚本是一项非常重要的功能。今天&#xff0c;我将为大家带来一款名为“定时执行专家”的软件的评测&#xff0c;特别是其定时执行VB…

IPSEC VPN安全介绍以及相关实验

目录 一、IPSEC相关的安全服务 二、IPSEC的安全协议 三、实验 IPSEC一组协议集合&#xff0c;用于确保在IP网络上进行通信时的安全性和保密性。它提供了一种标准化的方法&#xff0c;用于对IP数据包进行加密、身份验证和完整性保护。IPSEC通常用于建立虚拟私人网络VPN连接&am…

托管服务器常见的问题有哪些(常见托管问题解决)

​  对于我们来说&#xff0c;选择一个合适的服务器托管提供商只是建立和管理自己所属网站的第一步。相对来说&#xff0c;真正的挑战在于解决管理网站运行时可能出现的各种问题。就像有时&#xff0c;有些常见的服务器托管问题是不可避免的&#xff0c;但通过主动监控、定期…

Springboot教程(六)——异常处理

拦截异常 在Spring Boot中&#xff0c;我们可以将异常统一放在全局处理类来处理&#xff0c;创建一个全局异常处理类需要用到ControllerAdvice和ExceptionHandler注解。 ControllerAdvice类似一个增强版的Controller&#xff0c;用于标注类&#xff0c;表示该类声明了整个项目…

消息队列-Kafka-基础架构

基础架构 官网地址 上面这张图类比RocketMQ 相当于对一个主题进行了分区&#xff08;类似于RockeMQ 消息队列&#xff09;&#xff0c;每个分区存储到不同的Broker。在发送消息的时候都是发送到主分区。如果一台Broker由于其它节点备份了挂掉节点的数据&#xff0c;所以可以…

java-抢红包一些简单概念

抢红包&#xff0c;比如微信中抢红包&#xff0c;红包金额分配使用的是二倍均值算法。 二倍均值拆包&#xff1a; 拆包要求:所有人抢到的金额之和等于红包总额&#xff0c;每个人最少抢到 0.01 元&#xff0c;每个人抢到的红包金额不要相差太大二倍均值法:假设红包总金额是X&…

向量数据库短板及解决方案

索引内存消耗问题 索引的选择对于向量召回的性能至关重要&#xff0c;Milvus 支持了 Annoy&#xff0c;Faiss&#xff0c;HNSW等多种不同的索引&#xff0c;大部分索引必须加载进内存. 用户可以根据对延迟、内存使用和召回率的需求进行选择。对于大数据量&#xff0c;内存不足…

今日arXiv最热大模型论文:北大发布,通过上下文提示,让大模型学习一门新的语言

在人工智能领域&#xff0c;大语言模型&#xff08;LLMs&#xff09;的发展已经取得了显著的进步&#xff0c;它们在处理高资源语言方面表现出色。然而&#xff0c;对于资源较少的语言&#xff0c;尤其是极低资源语言&#xff0c;这些模型的支持却显得力不从心。这些语言往往缺…

数据结构测试题

目录 1.闰年判断 2.志愿者选拔 3.单词接龙 4.对称二叉树 5.英雄南昌欢迎您 6.时间转换 7.矩阵乘法 8. Huffuman树 1.闰年判断 题目描述&#xff1a; 给定一个年份&#xff0c;判断这一年是不是闰年。 当以下情况之一满足时&#xff0c;这一年是闰年&#xff1a; 1. 年…

Libevent的使用及reactor模型

Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库&#xff0c;主要有以下几个亮点&#xff1a;事件驱动&#xff08; event-driven&#xff09;&#xff0c;高性能;轻量级&#xff0c;专注于网络&#xff0c;不如 ACE 那么臃肿庞大&#xff1b;源代码相当精炼、易读…

Linux/Knife

Knife Enumeration nmap 第一次扫描发现系统对外开放了22和80端口&#xff0c;端口详细信息如下 系统对外开放了2个端口&#xff0c;22的ssh和80的http&#xff0c;先访问web看看 单看该服务&#xff0c;并没有发现有趣的东西&#xff0c;wappalyzer显示php版本为8.1.0 PHP…