JUC第二十一讲:JUC线程池:ScheduledThreadPoolExecutor详解

news2024/11/16 13:01:54

JUC线程池:ScheduledThreadPoolExecutor详解

本文是JUC第二十一讲,JUC线程池: ScheduledThreadPoolExecutor详解。在很多业务场景中,我们可能需要周期性的运行某项任务来获取结果,比如周期数据统计,定时发送数据等。在并发包出现之前,Java 早在1.3就提供了 Timer 类(只需要了解,目前已渐渐被 ScheduledThreadPoolExecutor 代替)来适应这些业务场景。随着业务量的不断增大,我们可能需要多个工作线程运行任务来尽可能的增加产品性能,或者是需要更高的灵活性来控制和监控这些周期业务。这些都是 ScheduledThreadPoolExecutor 诞生的必然性。

文章目录

  • JUC线程池:ScheduledThreadPoolExecutor详解
    • 1、带着BAT大厂的面试问题去理解ScheduledThreadPoolExecutor
    • 2、ScheduledThreadPoolExecutor简介
    • 3、ScheduledThreadPoolExecutor数据结构
    • 4、ScheduledThreadPoolExecutor 源码解析
      • 4.1、内部类ScheduledFutureTask
        • 1、属性
        • 2、核心方法 run()
        • 3、cancel方法
      • 4.2、核心属性
      • 4.3、构造函数
      • 4.4、核心方法:Schedule
      • 4.5、核心方法:scheduleAtFixedRate 和 scheduleWithFixedDelay
      • 4.6、核心方法:shutdown()
    • 5、再深入理解
    • 6、参考文章

1、带着BAT大厂的面试问题去理解ScheduledThreadPoolExecutor

请带着这些问题继续后文,会很大程度上帮助你更好的理解ScheduledThreadPoolExecutor。

  • ScheduledThreadPoolExecutor要解决什么样的问题? 定时任务
  • ScheduledThreadPoolExecutor相比ThreadPoolExecutor有哪些特性? 能执行周期任务
  • ScheduledThreadPoolExecutor有什么样的数据结构,核心内部类和抽象类?ThreadPoolExecutorScheduledFutureTaskDelayedWorkQueue
  • ScheduledThreadPoolExecutor有哪两个关闭策略? 区别是什么?run-after-shutdown 标识是否之后关闭后的任务
  • ScheduledThreadPoolExecutor中scheduleAtFixedRate 和 scheduleWithFixedDelay区别是什么?前者固定速率执行,后者固定延迟执行
  • 为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor? 后者固定核心线程数大小的线程池,并且使用了一个无界队列
  • Executors 提供了几种方法来构造 ScheduledThreadPoolExecutor?

2、ScheduledThreadPoolExecutor简介

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,为任务提供延迟或周期执行,属于线程池的一种。和 ThreadPoolExecutor 相比,它还具有以下几种特性:

  • 使用专门的任务类型—ScheduledFutureTask 来执行周期任务,也可以接收不需要时间调度的任务 (这些任务通过 ExecutorService 来执行)。
  • 使用专门的存储队列—DelayedWorkQueue 来存储任务,DelayedWorkQueue 是无界延迟队列 DelayQueue 的一种。相比ThreadPoolExecutor 也简化了执行机制(delayedExecute方法,后面单独分析)。
  • 支持可选的run-after-shutdown参数,在池被关闭(shutdown)之后支持可选的逻辑来决定是否继续运行周期或延迟任务。并且当任务(重新)提交操作与 shutdown 操作重叠时,复查逻辑也不相同。

3、ScheduledThreadPoolExecutor数据结构

img

ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor:

  • 详情请参考: JUC第二十讲:深入理解 Java 中的线程池

ScheduledThreadPoolExecutor 内部构造了两个内部类 ScheduledFutureTaskDelayedWorkQueue

  • ScheduledFutureTask:继承了FutureTask,说明是一个异步运算任务;最上层分别实现了Runnable、Future、Delayed接口,说明它是一个可以延迟执行的异步运算任务。
  • DelayedWorkQueue: 这是 ScheduledThreadPoolExecutor 为存储周期或延迟任务专门定义的一个延迟队列,继承了 AbstractQueue,为了契合 ThreadPoolExecutor 也实现了 BlockingQueue 接口。它内部只允许存储 RunnableScheduledFuture 类型的任务。与 DelayQueue 的不同之处就是它只允许存放 RunnableScheduledFuture 对象,并且自己实现了二叉堆(DelayQueue 是利用了 PriorityQueue 的二叉堆结构)。

4、ScheduledThreadPoolExecutor 源码解析

以下源码的解析是基于你已经理解了FutureTask。 FutureTask可以参考这篇文章:JUC第二十二讲:JUC线程池-FutureTask详解

4.1、内部类ScheduledFutureTask

1、属性
//为相同延时任务提供的顺序编号
private final long sequenceNumber;

//任务可以执行的时间,纳秒级
private long time;

//重复任务的执行周期时间,纳秒级。
private final long period;

//重新入队的任务
RunnableScheduledFuture<V> outerTask = this;

//延迟队列的索引,以支持更快的取消操作
int heapIndex;
  • sequenceNumber:当两个任务有相同的延迟时间时,按照 FIFO 的顺序入队。sequenceNumber 就是为相同延时任务提供的顺序编号。
  • time:任务可以执行时的时间,纳秒级,通过 triggerTime 方法计算得出。
  • period:任务的执行周期时间,纳秒级。正数表示固定速率执行(为 scheduleAtFixedRate 提供服务),负数表示固定延迟执行(为scheduleWithFixedDelay 提供服务),0表示不重复任务。
  • outerTask:重新入队的任务,通过reExecutePeriodic方法入队重新排序。
2、核心方法 run()
public void run() {
  	//是否为周期任务
    boolean periodic = isPeriodic();
  	//当前状态是否可以执行
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        //不是周期任务,直接执行
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        //设置下一次运行时间
      	setNextRunTime();
      	//重排序一个周期任务
        reExecutePeriodic(outerTask);
    }
}

说明:ScheduledFutureTask 的run方法重写了 FutureTask 的版本,以便执行周期任务时重置/重排序任务。任务的执行通过父类 FutureTask 的run实现。内部有两个针对周期任务的方法:

  • setNextRunTime():用来设置下一次运行的时间,源码如下:
// 设置下一次执行任务的时间
private void setNextRunTime() {
    long p = period;
  	// 固定速率执行,scheduleAtFixedRate
    if (p > 0)  
        time += p;
    else
      	// 固定延迟执行,scheduleWithFixedDelay
        time = triggerTime(-p);
}
// 计算固定延迟任务的执行时间
long triggerTime(long delay) {
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
  • reExecutePeriodic(): 周期任务重新入队等待下一次执行,源码如下:
// 重排序一个周期任务
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
  	// 池关闭后可继续执行
    if (canRunInCurrentRunState(true)) {
      	// 任务入列
        super.getQueue().add(task);
        // 重新检查run-after-shutdown参数,如果不能继续运行就移除队列任务,并取消任务的执行
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
          	//启动一个新的线程等待任务
            ensurePrestart();
    }
}

reExecutePeriodic 与 delayedExecute 的执行策略一致,只不过 reExecutePeriodic 不会执行拒绝策略而是直接丢掉任务。

3、cancel方法
public boolean cancel(boolean mayInterruptIfRunning) {
    boolean cancelled = super.cancel(mayInterruptIfRunning);
    if (cancelled && removeOnCancel && heapIndex >= 0)
        remove(this);
    return cancelled;
}

ScheduledFutureTask.cancel 本质上由其父类 FutureTask.cancel 实现。取消任务成功后会根据removeOnCancel参数决定是否从队列中移除此任务。

4.2、核心属性

// 关闭后继续执行已经存在的周期任务 
private volatile boolean continueExistingPeriodicTasksAfterShutdown;

// 关闭后继续执行已经存在的延时任务 
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;

// 取消任务后移除 
private volatile boolean removeOnCancel = false;

// 为相同延时的任务提供的顺序编号,保证任务之间的FIFO顺序
private static final AtomicLong sequencer = new AtomicLong();
  • continueExistingPeriodicTasksAfterShutdownexecuteExistingDelayedTasksAfterShutdown是 ScheduledThreadPoolExecutor 定义的 run-after-shutdown 参数,用来控制池关闭之后的任务执行逻辑。
  • removeOnCancel用来控制任务取消后是否从队列中移除。当一个已经提交的周期或延迟任务在运行之前被取消,那么它之后将不会运行。默认配置下,这种已经取消的任务在届期之前不会被移除。 通过这种机制,可以方便检查和监控线程池状态,但也可能导致已经取消的任务无限滞留。为了避免这种情况的发生,我们可以通过setRemoveOnCancelPolicy方法设置移除策略,把参数removeOnCancel设为true可以在任务取消后立即从队列中移除。
  • sequencer是为相同延时的任务提供的顺序编号,保证任务之间的 FIFO 顺序。与 ScheduledFutureTask 内部的 sequenceNumber参数作用一致。

4.3、构造函数

首先看下构造函数,ScheduledThreadPoolExecutor 内部有四个构造函数,这里我们只看这个最大构造灵活度的:

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

构造函数都是通过 super 调用了 ThreadPoolExecutor 的构造,并且使用特定等待队列 DelayedWorkQueue。

4.4、核心方法:Schedule

public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                       long delay,
                                       TimeUnit unit) {
    if (callable == null || unit == null)
        throw new NullPointerException();
  	// 构造 ScheduledFutureTask 任务
    RunnableScheduledFuture<V> t = decorateTask(callable,
        new ScheduledFutureTask<V>(callable, triggerTime(delay, unit)));
  	// 任务执行主方法  
  	delayedExecute(t);
    return t;
}

说明:schedule主要用于执行一次性(延迟)任务。函数执行逻辑分两步:

  • 封装 Callable/Runnable: 首先通过 triggerTime 计算任务的延迟执行时间,然后通过 ScheduledFutureTask 的构造函数把 Runnable/Callable 任务构造为ScheduledThreadPoolExecutor可以执行的任务类型,最后调用decorateTask方法执行用户自定义的逻辑;decorateTask是一个用户可自定义扩展的方法,默认实现下直接返回封装的 RunnableScheduledFuture 任务,源码如下:
protected <V> RunnableScheduledFuture<V> decorateTask (
    Runnable runnable, RunnableScheduledFuture<V> task) {
    return task;
}
  • 执行任务:通过 delayedExecute 实现。下面我们来详细分析。
private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
      	// 池已关闭,执行拒绝策略
        reject(task);
    else {
      	//任务入队
        super.getQueue().add(task);
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&//判断run-after-shutdown参数
            remove(task))//移除任务
            task.cancel(false);
        else
            ensurePrestart();//启动一个新的线程等待任务
    }
}

说明: delayedExecute是执行任务的主方法,方法执行逻辑如下:

  • 如果池已关闭(ctl >= SHUTDOWN),执行任务拒绝策略;
  • 池正在运行,首先把任务入队排序;然后重新检查池的关闭状态,执行如下逻辑:

A: 如果池正在运行,或者 run-after-shutdown 参数值为true,则调用父类方法ensurePrestart启动一个新的线程等待执行任务。ensurePrestart源码如下:

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

ensurePrestart 是父类 ThreadPoolExecutor 的方法,用于启动一个新的工作线程等待执行任务,即使corePoolSize为0也会安排一个新线程。

B: 如果池已经关闭,并且 run-after-shutdown 参数值为false,则执行父类(ThreadPoolExecutor)方法remove移除队列中的指定任务,成功移除后调用ScheduledFutureTask.cancel取消任务

4.5、核心方法:scheduleAtFixedRate 和 scheduleWithFixedDelay

/**
 * 创建一个周期执行的任务,第一次执行延期时间为initialDelay,
 * 之后每隔 period 执行一次,不等待第一次执行完成就开始计时
 */
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    // 构建 RunnableScheduledFuture 任务类型
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      // 计算任务的延迟时间
                                      triggerTime(initialDelay, unit),
                                      // 计算任务的执行周期
                                      unit.toNanos(period));
  	// 执行用户自定义逻辑
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
  	// 赋值给outerTask,准备重新入队等待下一次执行  
  	sft.outerTask = t;
  	// 执行任务
    delayedExecute(t);
    return t;
}

/**
 * 创建一个周期执行的任务,第一次执行延期时间为 initialDelay,
 * 在第一次执行完之后延迟delay后开始下一次执行
 */
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    // 构建RunnableScheduledFuture任务类型
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),//计算任务的延迟时间
                                      unit.toNanos(-delay));//计算任务的执行周期
  	// 执行用户自定义逻辑
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
  	// 赋值给outerTask,准备重新入队等待下一次执行
    sft.outerTask = t;
  	//执行任务
    delayedExecute(t);
    return t;
}

说明: scheduleAtFixedRate和scheduleWithFixedDelay方法的逻辑与schedule类似。

注意 scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别: 乍一看两个方法一模一样,其实,在unit.toNanos这一行代码中还是有区别的。没错,scheduleAtFixedRate传的是正值,而scheduleWithFixedDelay传的则是负值,这个值就是 ScheduledFutureTask 的period属性。

4.6、核心方法:shutdown()

public void shutdown() {
    super.shutdown();
}

// 取消并清除由于关闭策略 不应该运行的所有任务
@Override 
void onShutdown() {
    BlockingQueue<Runnable> q = super.getQueue();
    // 获取run-after-shutdown参数
    boolean keepDelayed =
        getExecuteExistingDelayedTasksAfterShutdownPolicy();
    boolean keepPeriodic =
        getContinueExistingPeriodicTasksAfterShutdownPolicy();
  	// 池关闭后不保留任务
    if (!keepDelayed && !keepPeriodic) {
        // 依次取消任务
        for (Object e : q.toArray())
            if (e instanceof RunnableScheduledFuture<?>)
                ((RunnableScheduledFuture<?>) e).cancel(false);
        q.clear();//清除等待队列
    }
  	// 池关闭后保留任务
    else {
        // Traverse snapshot to avoid iterator exceptions
        // 遍历快照以避免迭代器异常
        for (Object e : q.toArray()) {
            if (e instanceof RunnableScheduledFuture) {
                RunnableScheduledFuture<?> t = (RunnableScheduledFuture<?>)e;
                if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
                    t.isCancelled()) { // also remove if already cancelled
                    // 如果任务已经取消,移除队列中的任务
                    if (q.remove(t))
                        t.cancel(false);
                }
            }
        }
    }
  	// 终止线程池
    tryTerminate(); 
}

说明:池关闭方法调用了父类 ThreadPoolExecutor 的 shutdown,具体分析见 ThreadPoolExecutor 篇。这里主要介绍以下在shutdown 方法中调用的关闭钩子 onShutdown 方法,它的主要作用是在关闭线程池后取消并清除由于关闭策略不应该运行的所有任务,这里主要是根据 run-after-shutdown 参数 (continueExistingPeriodicTasksAfterShutdown 和executeExistingDelayedTasksAfterShutdown) 来决定线程池关闭后是否关闭已经存在的任务。

5、再深入理解

  • 为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor?

例如:由于 ScheduledThreadPoolExecutor 是一个固定核心线程数大小的线程池,并且使用了一个无界队列,所以调整maximumPoolSize 对其没有任何影响 (所以 ScheduledThreadPoolExecutor 没有提供可以调整最大线程数的构造函数,默认最大线程数固定为 Integer.MAX_VALUE)。此外,设置corePoolSize为0或者设置核心线程空闲后清除(allowCoreThreadTimeOut) 同样也不是一个好的策略,因为一旦周期任务到达某一次运行周期时,可能导致线程池内没有线程去处理这些任务。

  • Executors 提供了哪几种方法来构造 ScheduledThreadPoolExecutor?
    • newScheduledThreadPool:可指定核心线程数的线程池。
    • newSingleThreadScheduledExecutor: 只有一个工作线程的线程池。如果内部工作线程由于执行周期任务异常而被终止,则会新建一个线程替代它的位置。

注意:newScheduledThreadPool(1, threadFactory) 不等价于 newSingleThreadScheduledExecutor。newSingleThreadScheduledExecutor 创建的线程池保证内部只有一个线程执行任务,并且线程数不可扩展;而通过newScheduledThreadPool(1, threadFactory) 创建的线程池可以通过 setCorePoolSize 方法来修改核心线程数。

6、参考文章

  • JUC源码分析-线程池篇(三):ScheduledThreadPoolExecutor

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

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

相关文章

07_项目开发_用户信息列表

1 用户信息列表内容展示 用户信息列表&#xff0c;主要完成用户信息的添加、删除、修改和查找功能。 用户列表页面效果&#xff1a; 单击“添加用户”按钮&#xff0c;进入添加用户页面。 填写正确的信息后&#xff0c;单击“添加用户”按钮&#xff0c;会直接跳转到用户列表…

算法题:买卖股票的最佳时机 II (贪心算法解决股票问题)

这道题是贪心算法的中级难度练习题&#xff0c;由于题目设定&#xff0c;整个价格都是透明的&#xff0c;这里并不涉及需要预测股票涨势的问题。解决思路不难&#xff0c;就是一旦股票价格开始下降了就买入&#xff0c;一旦上升了&#xff0c;就赶紧卖出。&#xff08;完整题目…

算法题:盛最多水的容器(贪心算法双指针问题)

这个题目乍一看就是双指针&#xff0c;没想到官方解答也是双指针&#xff0c;我在官方的基础上优化了一下下&#xff0c;左右两边各一个指针&#xff0c;每次移动短的那一头的时候&#xff0c;不是移动一格&#xff0c;而是找到比短的那一头要长一点的&#xff0c;再进行比较。…

机器学习与模式识别作业----决策树属性划分计算

文章目录 1.决策树划分原理1.1.特征选择1--信息增益1.2.特征选择2--信息增益比1.3.特征选择3--基尼系数 2.决策树属性划分计算题2.1.信息增益计算2.2.1.属性1的信息增益计算2.2.2.属性2的信息增益计算2.2.3.属性信息增益比较 2.2.信息增益比计算2.3.基尼系数计算 1.决策树划分原…

小程序中使用echarts配置以及折线图案例(简单易懂)

第一步&#xff1a;引入echarts文件--此文件需要下载&#xff1a; 下载地址&#xff1a;点击此处进行下载echarts文件 点击Download ZIP下载压缩包&#xff0c;注意&#xff1a;此文件&#xff0c;我是从完整的文件中剥离出来的有用的&#xff0c;不会影响项目。 第二步&#…

# 解析Pikachu靶场:一个安全研究的练习场

引言 Pikachu靶场是一个非常流行的安全研究和渗透测试练习平台。这个环境包括多个安全漏洞&#xff0c;从基础的到高级的&#xff0c;供安全研究人员和渗透测试者进行实验和学习。在这篇博客中&#xff0c;我们将探讨Pikachu靶场的基本概念&#xff0c;功能&#xff0c;以及如…

vue3+vite+ts 组件中自动导入 ref 和 reactive

前言 在每个vue组件中&#xff0c;都去手动引入 ref 和 reactive 是非常繁琐的一件事&#xff0c;我们可以通过插件来完成自动导入 安装插件 npm i unplugin-auto-import -D 配置插件 在 vite.config.ts 中增加如下代码 import { defineConfig } from vite import vue fr…

初识操作系统以及Linux环境搭建

&#x1f4d9;作者简介&#xff1a; 清水加冰&#xff0c;目前大二在读&#xff0c;正在学习C/C、Python、操作系统、数据库等。 &#x1f4d8;相关专栏&#xff1a;C语言初阶、C语言进阶、C语言刷题训练营、数据结构刷题训练营、有感兴趣的可以看一看。 欢迎点赞 &#x1f44d…

大数据Splunk Enterprise 平台+cpolar 实现远程访问

文章目录 前言1. 搭建Splunk Enterprise2. windows 安装 cpolar3. 创建Splunk Enterprise公网访问地址4. 远程访问Splunk Enterprise服务5. 固定远程地址 前言 Splunk Enterprise是一个强大的机器数据管理平台&#xff0c;可帮助客户分析和搜索数据&#xff0c;以及可视化数据…

企架布道:中电金信应邀出席2023佛山敏捷之旅暨DevOps Meetup

近日&#xff0c;2023佛山敏捷之旅暨DevOps Meetup活动顺利举行&#xff0c;本次活动以助力大湾区金融和互联网企业敏捷DevOps实施和效能提升为主题&#xff0c;共设立 2个会场&#xff0c;16个话题分享&#xff0c;200余位金融、互联网企业相关从业人员齐聚一堂&#xff0c;共…

第二证券:市场情绪或逐步修复 十月行情值得期待

第二证券指出&#xff0c;周一A股商场探底回升、小幅轰动收拾&#xff0c;沪指全天底子出现先抑后扬的运转特征。其时上证综指与创业板指数的平均市盈率分别为12.46倍、33.94倍&#xff0c;处于近三年中位数以下水平&#xff0c;商场估值仍然处于较低区域&#xff0c;合适中长期…

excel单元格合并策略

excel单元格合并策略 证明112&#xff1f; 要证明112这个问题&#xff0c;首先我们要找到问题的关键。所谓问题的关键呢&#xff0c;就是关键的问题&#xff0c;那么如何找到问题的关键就是这个问题的关键。 比如说&#xff0c;你有一个苹果&#xff0c;我也有一个苹果&#x…

管理Linux的联网

1. RHEL9版本特点 在RHEL7版本中&#xff0c;同时支持network.service和NetworkManager.service&#xff08;简称NM&#xff09;。 在RHEL8上默认只能通过NM进行网络配置&#xff0c;包括动态ip和静态ip,若不开启NM&#xff0c;否则无法使用网络 RHEL8依然支持network.service&…

【工具软件】Nativefier——把网页打包成exe软件

官方文档 安装 npm install nativefier -g使用 在 nativefier 后加上需要转换的网站地址, 比如: nativefier "https://blog.csdn.net/IAIPython?typeblog"第一次打包需要下载 Eletron 框架, 很慢… 运行完毕, 会生成一个应用, 路径一般为C:\Users\用户名… 如图…

前后端分离项目-基于springboot+vue的图书馆管理系统的设计与实现(内含代码+文档+报告)

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

Servlet开发步骤

标准Java Web工程结构 pom.xml中提供servlet依赖 1.创建java类&#xff0c;继承HttpServlet 2.重写service方法&#xff0c;处理请求&#xff0c;生成响应 3.配置web.xml&#xff0c;绑定访问地址 Servlet接收请求参数 request.getParameter() 接收单个参数 request.ge…

区块链游戏的开发框架

链游&#xff08;Blockchain Games&#xff09;是基于区块链技术构建的游戏。它们与传统游戏有一些显著不同之处&#xff0c;因此需要特定的开发框架和工具。以下是一些用于链游开发的开发框架及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专…

一站式 API 管理和测试工具:PostCat 轻松完成接口调测 | 开源日报 No.49

rubickCenter/rubick Stars: 5.0k License: MIT Rubick 是一个基于 electron 开源工具箱的项目&#xff0c;它允许用户自由集成丰富插件来创建最终桌面效率工具。该项目以 Dota 英雄中的 Rubick 为名&#xff0c;因为他可以使用其他英雄作为插件完成任务。以下是 Rubick 的主要…

ant-design-vue 实现表格表头纵排列

结果如图&#xff1a; 区域&#xff0c;成功率&#xff0c;清单率为表头&#xff0c;右侧为动态的数据 废话不多说直接上代码&#xff1a; 1.先声明表格&#xff0c;使用框架自带a-table&#xff0c;核心点就在data和columns上 <div style"margin-bottom: 60px;"…

OpenVPN客户端安装测试

文章目录 一 OpenVPN客户端安装二 OpenVPN客户端设置三 OpenVPN客户端测试 一 OpenVPN客户端安装 OpenVPN有很多客户端&#xff0c;本文采用windows系统的OpenVPN Connect 3.4.2 (64-bit) 客户端进行安装和测试。 下载 下载地址&#xff1a;https://www.filehorse.com/downloa…