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

多线程——线程池

目录

·前言

一、什么是线程池

1.引入线程池的原因

2.线程池的介绍

二、标准库中的线程池

1.构造方法

2.方法参数

(1)corePoolSize 与 maximumPoolSize 

(2)keepAliveTime 与 unit

(3)workQueue(任务队列)

(4)threadFactory(线程工厂)

(5) handler(拒绝策略)

3.使用标准库中线程池

三、实现线程池

·结尾


·前言

        在我们学习编程知识过程中一定听说过很多池,比如常量池,还有在我前面 MySql 专栏中 JDBC 编程里提到的数据库连接池,以及本篇文章要为大家介绍的线程池,所谓的这些池作用其实都差不多,都是提前把要用的对象创建好,然后把用完的对象不立即释放留着以备下次使用,这样就可以起到提高效率的作用,本篇文章就会为大家介绍一下什么是线程池,在我们 Java 标准库中的线程池是什么样的,以及使用 Java 代码来实现一个简单的线程池让大家能更清晰的认识线程池,那么就开始本篇文章的介绍内容吧。

一、什么是线程池

1.引入线程池的原因

        在我们最开始,引入进程的概念就能够解决并发编程的问题,后来由于频繁创建销毁进程带来的开销太大,从而引入了线程(轻量级进程)这样的概念,使用复用资源的方式来提高创建销毁的效率,但是如果创建和销毁线程的频率也进一步提高呢?此时,线程的创建和开销也就不能无视了。

        为了优化线程的创建与销毁的效率,有下面两种解决方案:

  1. 引入轻量级线程,也称为“协程”;
  2. 使用线程池。

         为什么协程可以优化线程的开销与销毁,这是因为协程的本质是我们在用户态代码中进行调度,不是靠内核的调度器调度的,这样就可以节省很多调度上的开销,此时,我们代码中创建上千个线程会卡死,但是创建上千个协程就没什么事了。

        虽然协程有很多的好处,但是在 Java 中不是很推荐用上述做法来优化线程的创建与销毁,这是因为引入协程会引入额外的复杂性,使用协程可能不是很稳定,协程的调试比较困难……所以相比于协程,使用线程池对于优化线程的创建与销毁会更好一些,那么下面就进一步介绍一下线程池是什么吧。

2.线程池的介绍

        线程池就是要把使用的线程提前创建好,用完了一个线程也不要直接释放,而是放到线程池中以备下次的使用,这样就节省了线程创建与销毁的开销,因为在这使用线程的过程中,并没有真的频繁创建和销毁线程,只是从线程池中取线程使用,用完还会放回去。

        那么为什么从线程池中取线程就比从系统中申请更高效呢?这就好比你让室友帮你取快递,室友答应帮你取,但是什么时候给你取回来,他在帮你取快递的途中会不会做一些什么事情都是不确定的,相比之下,你自己去取快递,就会更高效,通过上面的例子,我们可以得到以下结论:

  • 从线程池中取线程是纯用户态代码,是可控的;
  • 通过系统申请创建线程,需要系统内核来完成,这是不可控的。

二、标准库中的线程池

1.构造方法

        在 Java 标准库中 ThreadPoolExecutor 这个类就是用来创建线程池的,关于这个类,它的构造方法有很多的参数,由我来给大家介绍一下,下面是这个类的几个构造方法,如下图所示:

        如上图,ThreadPoolExecutor 一共涉及到四个构造方法,这里我只对第四个构造方法的每个参数进行一个介绍,这是因为,最后一个构造方法的参数是最全的,可以这么理解,介绍完第四个构造方法的各个参数,其余三个构造方法也就都包含了。

2.方法参数

(1)corePoolSize 与 maximumPoolSize 

        在标准库提供的线程池中,持有的线程个数并不是一成不变的,它会根据当前的任务量来自适应当前线程的个数(任务数量很多,就会多创建几个线程,任务量比较少,就会少创建几个线程),在构造方法中的前两个参数 int corePoolSize 代表线程池中核心线程数有多少也就是一个线程池中最少得有多少个线程,int maximumPoolSize 代表了线程池中最大线程数是多少也就是一个线程池中最多能有多少个线程。

(2)keepAliveTime 与 unit

        第三个参数:long keepAliveTime 代表的意思就是线程池中除了核心线程外的线程的保持存活时间,在上面介绍了标准库中的线程池是根据当前的任务量来自适应当前线程的个数,这个参数就是自适应实现的一个重要标准,keepAliveTime 可以记录除了核心线程外的线程空闲的时间,如果这些线程的空闲时间超过了 keepAliveTime 的值就会自动销毁这些线程,来达到一个自适应的效果,这里的第四个参数:TimeUnit unit 就是搭配 keepAliveTime 这个参数的,unit 代表的是时间单位,它可以是 s、min、ms、hour……也就代表了空闲时间 keepAliveTime 的时间单位。

(3)workQueue(任务队列)

        第五个参数:BlockingQueue了<Runnable> workQueue 代表线程池中可以有很多个任务,这里使用 Runnable 来作为描述任务的主体,线程池中线程不断从这个阻塞队列中取任务来执行。

(4)threadFactory(线程工厂)

        第六个参数:ThreadFactory threadFactory 意思是线程工厂,通过这个工厂类就可以来创建线程对象(Thread 对象),这个类里提供了方法,方法中封装了 new Thread 这样的操作,并且同时给 Thread 设置了一些属性,由此就构成了 ThreadFactory 线程工厂。 

        这里的线程工厂也用到了一种设计模式:工厂模式,它是通过专门的“工厂类”/“工厂对象”来创建指定对象,那么为什么使用工厂模式呢?我们来看下面的一个代码示例:

// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public Point(double x, double y) {setX(x);setY(y);}// 通过极坐标系来构造这个点public Point(double r, double a) {setR(r);setA(a);}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}

        不知道大家看完上面的代码有没有发现什么问题,这里的问题就在于 Point 这个类的两个构造方法不构成重载,它们的参数列表是一样的,如下图所示: 

        想必我们都知道,使用笛卡尔坐标和使用极坐标都可以表示一个点,并且这两个表示方法并不相同,想通过同一个类的构造方法来用这两种不同的方式表示不同的点就违背了 Java 的语法规则,为了解决上述的问题,就引入了“工厂模式”。

        工厂模式的基本逻辑就是使用普通方法来创建对象,在普通方法中把构造方法进行封装,利用工厂模式修改后的代码及运行结果如下所示:

// 表示平面上的一个点
class Point {// 笛卡尔坐标系 x 与 yprivate double x;private double y;// 极坐标系 r 与 aprivate double r;private double a;// 通过笛卡尔坐标系来构造这个点public static Point makePointByXY(double x, double y) {Point point = new Point();point.setX(x);point.setY(y);return point;}// 通过极坐标系来构造这个点public static Point makePointByRA(double r, double a) {Point point = new Point();point.setR(r);point.setA(a);return point;}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}
public class PointFactory {public static void main(String[] args) {Point point1 = Point.makePointByXY(3,4);Point point2 = Point.makePointByRA(3,30);System.out.println("point1 的笛卡尔坐标:->(" + point1.x() + "," + point1.y() + ")");System.out.println("point2 的极坐标:->(" + point2.r() + "," + point2.a() + ")");}
}

 

         此时,利用工厂模式就可以创建出两个方式表示的点,代码中的 makePointByXY 方法与 makePointByRA 方法也称为工厂方法,如果把工厂方法放到一个其他的类中,这个类就叫做“工厂类”,总的来说,通过静态方法封装 new 操作,在方法内部设定不同的属性来完成对象的初始化,构造对象的过程,就是工厂模式。

(5) handler(拒绝策略)

        第七个参数:RejectedExecutionHandler handler 这个参数可以算是最重要的一个参数,在前面介绍的第五个参数:BlockingQueue了<Runnable> workQueue 这是线程池中的一个阻塞队列,用来存储当前线程池要执行的任务都有哪些,它能够容纳的元素是有上限的,此时当这个阻塞队列中的任务已经排满了,还有新的任务要往这个阻塞队列中添加,线程池该怎么办?这就需要我们的第七个参数:RejectedExecutionHandler handler 来指明一个拒绝策略,如下图所示:

        上图中的这四个类也就代表了四种拒绝策略,它们所对应的拒绝策略如下表所示:

拒绝策略

ThreadPoolExecutor.AbortPolicy

继续添加任务,直接抛出异常。
ThreadPoolExecutor.CallerRunsPolicy新的任务由添加任务的线程负责执行。
ThreadPoolExecutor.DiscardOldestPolicy丢弃最老的任务,添加新的任务。
ThreadPoolExecutor.DiscardPolicy丢弃最新的任务。

3.使用标准库中线程池

        上面介绍了 ThreadPoolExecutor 类的构造方法及构造方法中的参数,可以看出来 ThreadPoolExecutor 类本身用起来比较复杂,因此在标准库中还提供了另一个版本的线程池,也就是把 ThreadPoolExecutor 类给封装了一下,这个线程池就是 Executors 工厂类,通过这个类来创建出的不同线程池对象,Executors 类在内部把 ThreadPoolExecutor 创建好了,并且设置了不同的参数,下面就使用 Executors 演示一下标准库中线程池的效果吧。

        如下图所示,在 Executors 中内置了很多版本的线程池,这里我们使用固定数目的线程池来简单演示一下线程池的效果即可。

        下面使用线程池的具体代码及运行结果如下所示: 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestDemo7 {public static void main(String[] args) {// ExecutorService 提供了一种管理和控制异步任务执行的方式ExecutorService service = Executors.newFixedThreadPool(4);// 使用 submit 方法把任务添加到线程池中service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}

         介绍完这两个标准库中的线程池,可以明确一点,当我们只是想简单用一下线程池,就可以使用 Executors ,当我们希望高度定制化一个线程池,就可以使用 ThreadPoolExecutor。

三、实现线程池

        在前面介绍了标准库中的线程池及演示了使用的效果,下面我就来写代码实现一个简单的线程池,这里我就直接写一个固定线程数目的线程池,下面是这个简单线程池中包含的内容:

  1. 提供构造方法,指定创建多少个线程;
  2. 在构造方法中,把这些线程都创建好;
  3. 创建一个阻塞队列,能够持有要执行的任务;
  4. 提供 submit 方法,可以添加新的任务。

        那么下面我就直接上代码了,关于这个简单线程池实现的细节我会在代码中以注释的方式进行介绍,线程池实现的代码及运行结果如下所示:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 创建阻塞队列,用来接收任务,这里设置最多容纳任务量为 100private BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(100);// 创建线程链表,把创建的每个线程都用线程链表组织起来private List<Thread> threadList = new ArrayList<>();// 构造方法,指定线程池中固定的线程数,并且将线程都创建好public MyThreadPoolExecutor(int num) {for (int i = 0; i < num; i++) {Thread t = new Thread(()->{while (true) {// 利用 runnable 来接收阻塞队列中的任务Runnable runnable = null;try {// 获取任务runnable = blockingQueue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 执行任务runnable.run();}});// 启动线程t.start();// 将线程加入到线程链表中threadList.add(t);}}// 方法 sumbit 用来向阻塞队列中添加新的任务public void sumbit(Runnable runnable) throws InterruptedException {blockingQueue.put(runnable);}
}public class ThreadDemo8 {public static void main(String[] args) throws InterruptedException {// 创建线程池,指定线程数目为 4MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);// 循环 100 次,向线程池中添加 100 个任务for (int i = 0; i < 100; i++) {int n = i;executor.sumbit(new Runnable() {@Overridepublic void run() {// 任务的内容System.out.println("执行任务:->" + n + ",执行的线程是:->" + Thread.currentThread().getName());}});}}
}

        如上图的运行结果可以看出,多个线程之间的执行顺序是不确定的,某个线程获取到了某个任务,但并非是立即执行,在这个过程中很有可能另一个线程就插到前面了,这里的这些线程彼此之间都是等价的。

·结尾

        文章到这里就要结束了,回顾本篇文章,我介绍了什么是线程池,标准库中线程池还有实现了一个简单的线程池,其中还是要多理解一下标准库中线程池构造方法每个参数的意思,及理解拒绝策略的含义,这可以让我们对 ThreadPoolExecutor 类的使用更加清晰,后面实现的线程池也就可以看出线程池基本的工作原理,那就是不断利用这 4 个线程来执行任务,这样就省去创建和销毁线程的开销,那么如果你感觉本篇文章对你有所帮助,还是希望能收到你的三连鼓励,如果对文章的内容有所疑问欢迎在评论区进行讨论,我们下一篇文章再见吧~~~

相关文章:

多线程——线程池

目录 前言 一、什么是线程池 1.引入线程池的原因 2.线程池的介绍 二、标准库中的线程池 1.构造方法 2.方法参数 &#xff08;1&#xff09;corePoolSize 与 maximumPoolSize &#xff08;2&#xff09;keepAliveTime 与 unit &#xff08;3&#xff09;workQueue&am…...

VScode插件:前端每日一题

大文件上传如何做断点续传&#xff1f; 在前端实现大文件上传的断点续传&#xff0c;通常会将文件切片并分块上传&#xff0c;记录每块的上传状态&#xff0c;以便在中断或失败时只上传未完成的部分。以下是实现断点续传的主要步骤和思路&#xff1a; 1. 文件切片 (File Slici…...

Android跨进程通信

1、跨进程通信的几种方式 在 Android 中&#xff0c;跨进程通信 (IPC, Inter-Process Communication) 方式有多种&#xff0c;主要用于在不同的应用或进程之间传递数据。常见的跨进程通信方式包括&#xff1a; AIDL (Android Interface Definition Language) • 描述&#xff…...

【初阶数据结构】计数排序 :感受非比较排序的魅力

文章目录 前言1. 什么是计数排序&#xff1f;2. 计数排序的算法思路2.1 绝对位置和相对位置2.2 根据计数数组的信息来确认 3. 计数排序的代码4. 算法分析5. 计数排序的优缺点6.计数排序的应用场景 前言 如果大家仔细思考的话&#xff0c;可能会发现这么一个问题。我们学的七大…...

前后双差速轮之LQR控制

在之前的代码中,我们实现了前后两对双差速轮AGV的运动学正解和逆解。但为了实现对AGV的精确路径跟踪和姿态控制,我们需要引入控制算法。线性二次型调节器(LQR)是一种常用的最优控制方法,可以有效地将系统的状态误差最小化。本文将详细说明如何在之前的C++代码中加入LQR控制…...

Linux之远程连接服务器

1、远程连接服务器简介 &#xff08;1&#xff09;什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机以取得可操作主机接口&#xff08;shell&#xff09;&#xff0c;而登录后的操作感觉就像是坐在系统前面…...

k8s 部署 nexus3 详解

创建命名空间 nexus3-namespace.yaml apiVersion: v1 kind: Namespace metadata:name: nexus-ns创建pv&pvc nexus3-pv-pvc.yaml apiVersion: v1 kind: PersistentVolume metadata:name: nfs-pvnamespace: nexus-ns spec:capacity:storage: 3GiaccessModes:- ReadWriteM…...

从“摸黑”到“透视”:AORO A23热成像防爆手机如何改变工业检测?

在工业检测领域&#xff0c;传统的检测手段常因效率低下、精度不足和潜在的安全风险而受到诟病。随着科技的不断进步&#xff0c;一种新兴的检测技术——红外热成像技术&#xff0c;正逐渐在该领域崭露头角。近期&#xff0c;小编对一款集成红外热成像技术的AORO A23防爆手机进…...

让你的 IDEA 使用更流畅 | IDEA内存修改

随着idea使用越来越频繁&#xff0c;笔者最近发现使用过程中有时候会出现卡顿现象&#xff0c;例如&#xff0c;启动软件变慢&#xff0c;打开项目的速度变慢等&#xff1a; 因此如果各位朋友觉得最近也遇到了同样的困惑&#xff0c;不妨跟着笔者一起来设置IDEA的内存大小吧~ …...

docker run 命令解析

docker run 命令解析 docker run 命令用于从给定的镜像启动一个新的容器。这个命令可以包含许多选项&#xff0c;下面是一些常用的选项&#xff1a; -d&#xff1a;后台运行容器&#xff0c;并返回容器ID&#xff1b;-i&#xff1a;以交互模式运行容器&#xff0c;通常与 -t …...

[Unity Demo]从零开始制作空洞骑士Hollow Knight第十七集:制作第一个BOSS苍蝇之母

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、战斗场景Battle Scene相关逻辑处理 1.防止玩家走出战斗场景的门2.制作一个简单的战斗场景二、制作游戏第一个BOSS苍蝇之母 1.导入素材和制作相关动画2.制作…...

【Nginx系列】499错误

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

Springboot项目控制层注释

Springboot主流的 ----------------------- 简略写法 package com.dx.wlmq.controller;import com.dx.wlmq.domain.Address; import com.dx.wlmq.service.AddresssService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.b…...

从Docker容器中备份整个PostgreSQL

问题 现在需要从Docker容器中备份整个PostgreSQL后&#xff0c;然后&#xff0c;使用备份文件在另外一个pg的docker容器中恢复过来。 步骤 备份旧容器中的PG # 登录到旧的PG容器中 docker exec -it postgres bash # 备份数据库 pg_dumpall -c -U postgres > dump_date %…...

从小需求看大格局:如何用技术智慧赢得客户信任

时间&#xff1a;2024年 10月 26日 作者&#xff1a;小蒋聊技术 邮箱&#xff1a;wei_wei10163.com 微信&#xff1a;wei_wei10 音频&#xff1a;从小需求看大格局&#xff1a;如何用技术智慧赢得客户信任 欢迎大家回到“小蒋聊技术”&#xff0c;这是一个不只是教你如何写…...

模型 支付矩阵

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。策略选择的收益分析工具。 1 支付矩阵的应用 1.1 支付矩阵在市场竞争策略分析中的应用 支付矩阵是一种强大的决策工具&#xff0c;它在多个领域的应用中都发挥着重要作用。以下是一个具体的应用案例…...

擎创科技声明

近日&#xff0c;我司陆续接到求职者反映&#xff0c;有自称是擎创科技招聘人员&#xff0c;冒用“上海擎创信息技术有限公司”名义&#xff0c;用“126.com”的邮箱向求职者发布招聘信息&#xff0c;要求用户下载注册APP&#xff0c;进行在线测评。 对此&#xff0c;我司郑重…...

二叉树习题其六【力扣】【算法学习day.13】

前言 书接上篇文章二叉树习题其四&#xff0c;这篇文章我们将基础拓展 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一…...

互联网的无形眼睛:浏览器指纹与隐私保护攻略

你是否曾有过这样的经历&#xff1a;在某个电商网站上搜索了某件商品&#xff0c;随后无论你打开哪个网页&#xff0c;都能看到与之相关的广告&#xff1f;或者当你再次访问某个网站时&#xff0c;它居然记得你之前的浏览记录&#xff1f;这一切&#xff0c;背后都有一只“看不…...

后端技术:有哪些常见的应用场景?

篇一、 原文链接&#xff1a;https://www.zhihu.com/question/642709585/answer/3388752666 1、数据处理和存储 后端技术可用于处理和存储大量数据&#xff0c;例如构建数据库系统、设计高效的数据结构、实现算法等。常见的数据库技术有关系型数据库&#xff08;如MySQL、O…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

Unity3D中Gfx.WaitForPresent优化方案

前言 在Unity中&#xff0c;Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染&#xff08;即CPU被阻塞&#xff09;&#xff0c;这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案&#xff1a; 对惹&#xff0c;这里有一个游戏开发交流小组&…...

23-Oracle 23 ai 区块链表(Blockchain Table)

小伙伴有没有在金融强合规的领域中遇见&#xff0c;必须要保持数据不可变&#xff0c;管理员都无法修改和留痕的要求。比如医疗的电子病历中&#xff0c;影像检查检验结果不可篡改行的&#xff0c;药品追溯过程中数据只可插入无法删除的特性需求&#xff1b;登录日志、修改日志…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

大型活动交通拥堵治理的视觉算法应用

大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动&#xff08;如演唱会、马拉松赛事、高考中考等&#xff09;期间&#xff0c;城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例&#xff0c;暖城商圈曾因观众集中离场导致周边…...

React Native在HarmonyOS 5.0阅读类应用开发中的实践

一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强&#xff0c;React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 &#xff08;1&#xff09;使用React Native…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...