12分钟从Executor自顶向下彻底搞懂线程池

news2025/1/18 11:56:12

通读本篇文章前先来看看几个问题,看看你是否已经理解线程池

  1. 什么是池化技术?它有什么特点,哪些场景使用?
  2. Executor是什么?它的设计思想是什么样的?
  3. 工作任务有几种?有什么特点?如何适配然后交给Executor的?
  4. 线程池是如何实现的?有哪些核心参数,该如何配置?工作流程是怎样的?
  5. 线程池如何优雅的处理异常?如何关闭线程池?
  6. 处理定时的线程池是如何实现的?

池化技术

线程的创建、销毁都会带来一定的开销

如果当我们需要使用到多线程时再去创建,使用完又去销毁,这样去使用不仅会拉长业务流程,还会增加创建、销毁线程的开销

于是有了池化技术的思想,将线程提前创建出来,放在一个池子(容器)中进行管理

当需要使用时,从池子里拿取一个线程来执行任务,执行完毕后再放回池子

不仅是线程有池化的思想,连接也有池化的思想,也就是连接池

池化技术不仅能复用资源、提高响应,还方便管理

Executor框架

Executor框架是什么?

可以暂时把Executor看成线程池的抽象,它定义如何去执行任务

  public interface Executor {
      void execute(Runnable command);
  }

Executor将工作任务与线程池进行分离解耦

工作任务被分为两种:无返回结果的Runnable和有返回结果的Callable

在线程池中允许执行这两种任务,其中它们都是函数式接口,可以使用lambda表达式来实现

有的同学可能会有疑问,上文Executor框架定义的执行方法不是只允许传入Runnable任务吗?

那Callable任务调用哪个方法来执行呢?

Future接口用来定义获取异步任务的结果,它的实现类常是FutureTask

FutureTask实现Runnable的同时,还用字段存储Callable,在其实现Runnable时实际上会去执行Callable任务

线程池在执行Callable任务时,会将使用FutureTask将其封装成Runnable执行(具体源码我们后面再聊),因此Executor的执行方法入参只有Runnable

FutureTask相当于适配器,将Callable转换为Runnable再进行执行

Executor 定义线程池,而它的重要实现是ThreadPoolExecutor

在ThreadPoolExecutor的基础上,还有个做定时的线程池ScheduledThreadPoolExecutor

ThreadPoolExecutor

核心参数

ThreadPoolExecutor主要有七个重要的参数

  public ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue,
                                ThreadFactory threadFactory,
                                RejectedExecutionHandler handler)
  1. corePoolSize 线程池核心线程数量
  2. maximumPoolSize 线程池允许创建的最大线程数
  3. keepAliveTime 超时时间,TimeUnit时间单位:非核心线程空闲后存活的时间
  4. workQueue 存放等待执行任务的阻塞队列
  5. threadFactory线程工厂:规定如何创建线程,可以根据业务不同规定 不同的线程组名称
  6. RejectedExecutionHandler 拒绝策略:当线程不够用,并且阻塞队列爆满时如何拒绝任务的策略
拒绝策略作用
AbortPolicy 默认抛出异常
CallerRunsPolicy调用线程来执行任务
DiscardPolicy不处理,丢弃
DiscardOldestPolicy丢弃队列中最近一个任务,并立即执行当前任务

线程池中除了构造时的核心参数外,还使用内部类Worker来封装线程和任务,并使用HashSet容器workes工作队列存储工作线程worker

实现原理

流程图

为了清晰的理解线程池实现原理,我们先用流程图和总结概述原理,最后来看源码实现

  1. 如果工作线程数量小于核心线程数量,创建线程、加入工作队列、执行任务
  2. 如果工作线程数量大于等于核心线程数量并且线程池还在运行则尝试将任务加入阻塞队列
  3. 如果任务加入阻塞队列失败(说明阻塞队列已满),并且工作线程小于最大线程数,则创建线程执行
  4. 如果阻塞队列已满、并且工作线程数量达到最大线程数量则执行拒绝策略

execute

线程池有两种提交方式execute和submit,其中submit会封装成RunnableFuture最终都来执行execute

      public <T> Future<T> submit(Callable<T> task) {
          if (task == null) throw new NullPointerException();
          RunnableFuture<T> ftask = newTaskFor(task);
          execute(ftask);
          return ftask;
      }

execute中实现线程池的整个运行流程

  public void execute(Runnable command) {
      //任务为空直接抛出空指针异常
      if (command == null)
          throw new NullPointerException();
      //ctl是一个整型原子状态,包含workerCount工作线程数量 和 runState是否运行两个状态
      int c = ctl.get();
      //1.如果工作线程数 小于 核心线程数 addWorker创建工作线程
      if (workerCountOf(c) < corePoolSize) {
          if (addWorker(command, true))
              return;
          c = ctl.get();
      }
      
      // 2.工作线程数 大于等于 核心线程数时
      // 如果 正在运行 尝试将 任务加入队列
      if (isRunning(c) && workQueue.offer(command)) {
          //任务加入队列成功 检查是否运行
          int recheck = ctl.get();
          //不在运行 并且 删除任务成功 执行拒绝策略 否则查看工作线程为0就创建线程
          if (! isRunning(recheck) && remove(command))
              reject(command);
          else if (workerCountOf(recheck) == 0)
              addWorker(null, false);
      }
      // 3.任务加入队列失败,尝试去创建非核心线程,成功则结束
      else if (!addWorker(command, false))
          // 4.失败则执行拒绝策略
          reject(command);
  }

addWorker

addWorker用于创建线程加入工作队列并执行任务

第二个参数用来判断是不是创建核心线程,当创建核心线程时为true,创建非核心线程时为false

  private boolean addWorker(Runnable firstTask, boolean core) {
          //方便跳出双层循环
          retry:
          for (;;) {
              int c = ctl.get();
              int rs = runStateOf(c);
  
              // Check if queue empty only if necessary.
              // 检查状态
              if (rs >= SHUTDOWN &&
                  ! (rs == SHUTDOWN &&
                     firstTask == null &&
                     ! workQueue.isEmpty()))
                  return false;
  
              for (;;) {
                  int wc = workerCountOf(c);
                  //工作线程数已满 返回false 
                  if (wc >= CAPACITY ||
                      wc >= (core ? corePoolSize : maximumPoolSize))
                      return false;
                  //CAS自增工作线程数量 成功跳出双重循环
                  if (compareAndIncrementWorkerCount(c))
                      break retry;
                  //CAS失败 重新读取状态 内循环
                  c = ctl.get();  // Re-read ctl
                  if (runStateOf(c) != rs)
                      continue retry;
                  // else CAS failed due to workerCount change; retry inner loop
              }
          }
  
          //来到这里说明已经自增工作线程数量 准备创建线程
          boolean workerStarted = false;
          boolean workerAdded = false;
          Worker w = null;
          try {
              //创建worker 通过线程工厂创建线程
              w = new Worker(firstTask);
              final Thread t = w.thread;
              if (t != null) {
                  //全局锁
                  final ReentrantLock mainLock = this.mainLock;
                  mainLock.lock();
                  try {
                      // Recheck while holding lock.
                      // Back out on ThreadFactory failure or if
                      // shut down before lock acquired.
                      int rs = runStateOf(ctl.get());
  
                      if (rs < SHUTDOWN ||
                          (rs == SHUTDOWN && firstTask == null)) {
                          if (t.isAlive()) // precheck that t is startable
                              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;
      }

addWorker中会CAS自增工作线程数量,创建线程再加锁,将线程加入工作队列workes(hashset),解锁后开启该线程去执行任务

runWorker

worker中实现Runnable的是runWorker方法,在启动线程后会不停的执行任务,任务执行完就去获取任务执行

  final void runWorker(Worker w) {
      Thread wt = Thread.currentThread();
      Runnable task = w.firstTask;
      w.firstTask = null;
      w.unlock(); // allow interrupts
      boolean completedAbruptly = true;
      try {
          //循环执行任务 getTask获取任务
          while (task != null || (task = getTask获取任务()) != null) {
              w.lock();
              // If pool is stopping, ensure thread is interrupted;
              // if not, ensure thread is not interrupted.  This
              // requires a recheck in second case to deal with
              // shutdownNow race while clearing interrupt
              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);
      }
  }

在执行前后预留两个钩子空方法,留给子类来扩展,后文处理线程池异常也会用到

配置参数

线程池中是不是越多线程就越好呢?

首先,我们要明白创建线程是有开销的,程序计数器、虚拟机栈、本地方法栈都是线程私有的空间

并且线程在申请空间时,是通过CAS申请年轻代的Eden区中一块内存(因为可能存在多线程同时申请所以要CAS)

线程太多可能导致Eden空间被使用太多导致young gc,并且线程上下文切换也需要开销

因此,线程池中线程不是越多越好,行业内分为两种大概方案

针对CPU密集型,线程池设置最大线程数量为CPU核心数量+1,避免上下文切换,提高吞吐量,多留一个线程兜底

针对IO密集型,线程池设置最大线程数量为2倍CPU核心数量,由于IO需要等待,为了避免CPU空闲就多一些线程

具体业务场景需要具体分析,然后加上大量测试才能得到最合理的配置

Executor框架通过静态工厂方法提供几种线程池,比如:Executors.newSingleThreadExecutor()、Executors.newFixedThreadPool()、Executors.newCachedThreadPool()

但由于业务场景的不同,最好还是自定义线程池;当理解线程池参数和实现原理后,查看它们的源码并不难,我们不过多叙述

处理异常

线程池中如果出现异常会怎么样?

Runnable

当我们使用Runnable任务时,出现异常会直接抛出

         threadPool.execute(() -> {
             int i = 1;
             int j = 0;
             System.out.println(i / j);
         });

面对这种情况,我们可以在Runnable任务中使用try-catch进行捕获

         threadPool.execute(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 System.out.println(i / j);
             } catch (Exception e) {
                 System.out.println(e);
             }
         });

实际操作的话用日志记录哈,不要打印到控制台

Callable

当我们使用Callable任务时,使用submit方法会获取Future

         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });

如果不使用Future.get()去获取返回值,那么异常就不会抛出,这是比较危险的

为什么会出现这样的情况呢?

前文说过执行submit时会将Callable封装成FutureTask执行

在其实现Runnable中,在执行Callable任务时,如果出现异常会封装在FutureTask中

     public void run() {
         //...其他略
         try {
             //执行call任务
             result = c.call();
             ran = true;
         } catch (Throwable ex) {
             //出现异常 封装到FutureTask
             result = null;
             ran = false;
             setException(ex);
         }
         //..
     }

等到执行get时,先阻塞、直到完成任务再来判断状态,如果状态不正常则抛出封装的异常

     private V report(int s) throws ExecutionException {
         Object x = outcome;
         if (s == NORMAL)
             return (V)x;
         if (s >= CANCELLED)
             throw new CancellationException();
         throw new ExecutionException((Throwable)x);
     }

因此在处理Callable任务时,可以对任务进行捕获也可以对get进行捕获

         //捕获任务
         Future<?> f = threadPool.submit(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 return i / j;
             } catch (Exception e) {
                 System.out.println(e);
             } finally {
                 return null;
             }
         });
 
         //捕获get
         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });
 ​
         try {
             Integer integer = future.get();
         } catch (Exception e) {
             System.out.println(e);
         }

afterExecutor

还记得线程池的runWorker吗?

它在循环中不停的获取阻塞队列中的任务执行,在执行前后预留钩子方法

继承ThreadPoolExecutor来重写执行后的钩子方法,记录执行完是否发生异常,如果有异常则进行日志记录,作一层兜底方案

 public class MyThreadPool extends ThreadPoolExecutor {  
     //...
     
     @Override
     protected void afterExecute(Runnable r, Throwable t) {
         //Throwable为空 可能是submit提交 如果runnable为future 则捕获get
         if (Objects.isNull(t) && r instanceof Future<?>) {
             try {
                 Object res = ((Future<?>) r).get();
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             } catch (ExecutionException e) {
                 t = e;
             }
         }
 ​
         if (Objects.nonNull(t)) {
             System.out.println(Thread.currentThread().getName() + ": " + t.toString());
         }
     }
 }

这样即使使用submit,忘记使用get时,异常也不会“消失”

setUncaughtException

创建线程时,可以设置未捕获异常uncaughtException方法,当线程出现异常未捕获时调用,也可以打印日志作兜底

我们定义我们自己的线程工厂,以业务组group为单位,创建线程(方便出错排查)并设置uncaughtException方法

 public class MyThreadPoolFactory implements ThreadFactory {
 ​
     private AtomicInteger threadNumber = new AtomicInteger(1);
     
     private ThreadGroup group;
 ​
     private String namePrefix = "";
 ​
     public MyThreadPoolFactory(String group) {
         this.group = new ThreadGroup(group);
         namePrefix = group + "-thread-pool-";
     }
 ​
 ​
     @Override
     public Thread newThread(Runnable r) {
         Thread t = new Thread(group, r,
                 namePrefix + threadNumber.getAndIncrement(),
                 0);
         t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
             @Override
             public void uncaughtException(Thread t, Throwable e) {
                 System.out.println(t.getName() + ":" + e);
             }
         });
 ​
         if (t.isDaemon()) {
             t.setDaemon(false);
         }
         if (t.getPriority() != Thread.NORM_PRIORITY) {
             t.setPriority(Thread.NORM_PRIORITY);
         }
         return t;
     }
 ​
 }

关闭线程池

关闭线程池的2种方法: shutdown(),shutdownNow()

它们的原理都是: 遍历工作队列wokers中的线程,逐个中断(调用线程的interrupt方法) 无法响应中断的任务可能永远无法终止

shutdown 任务会被执行完
  1. 将线程池状态设置为SHUTDOWN
  2. 中断所有未正在执行任务的线程
shutdownNow 任务不一定会执行完
  1. 将线程池状态设置为STOP
  2. 尝试停止所有正在执行或暂停任务的线程
  3. 返回等待执行任务列表

通常使用shutdown,如果任务不一定要执行完可以使用shutdownNow

SecheduledThreadPoolExecutor

ScheduledThreadPoolExecutor在ThreadPoolExecutor的基础上提供定时执行的功能

它有两个定时的方法

scheduleAtFixedRate 以任务开始为周期起点,比如说一个任务执行要0.5s,每隔1s执行,相当于执行完任务过0.5s又开始执行任务

scheduledWithFixedDelay 以任务结束为周期起点,比如说一个任务执行要0.5s,每隔1s执行,相当于执行完任务过1s才开始执行任务

         ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);
         //scheduleAtFixedRate 固定频率执行任务 周期起点为任务开始
         scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduleAtFixedRate 周期起点为任务开始");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);
 ​
         //scheduledWithFixedDelay 固定延迟执行任务,周期起点为任务结束
         scheduledThreadPoolExecutor.scheduleWithFixedDelay(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduledWithFixedDelay 周期起点为任务结束 ");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);

定时线程池使用延迟队列充当阻塞队列实现的

延迟队列是一个优先级队列,它排序存储定时任务,时间越小越先执行

线程获取任务时,会从延迟队列中获取定时任务,如果时间已到就执行

     public RunnableScheduledFuture<?> take() throws InterruptedException {
             final ReentrantLock lock = this.lock;
             lock.lockInterruptibly();
             try {
                 for (;;) {
                     RunnableScheduledFuture<?> first = queue[0];
                     //没有定时任务 等待
                     if (first == null)
                         available.await();
                     else {
                         //获取延迟时间
                         long delay = first.getDelay(NANOSECONDS);
                         //小于等于0 说明超时,拿出来执行
                         if (delay <= 0)
                             return finishPoll(first);
                         first = null; // don't retain ref while waiting
                         //当前线程是leader则等待对应的延迟时间,再进入循环取出任务执行
                         //不是leader则一直等待,直到被唤醒
                         if (leader != null)
                             available.await();
                         else {
                             Thread thisThread = Thread.currentThread();
                             leader = thisThread;
                             try {
                                 available.awaitNanos(delay);
                             } finally {
                                 if (leader == thisThread)
                                     leader = null;
                             }
                         }
                     }
                 }
             } finally {
                 if (leader == null && queue[0] != null)
                     available.signal();
                 lock.unlock();
             }
         }

这两个定时方法一个以任务开始为周期起点、另一个以任务结束为周期起点

获取定时任务的流程是相同的,只是它们构建的定时任务中延迟的时间不同

定时任务使用period 区别,为正数周期起点为任务开始,为负数时周期起点为任务结束

总结

本篇文章围绕线程池,深入浅出的讲解池化技术,Executor,线程池的参数、配置、实现原理、处理异常、关闭等

使用池化技术能够节省频繁创建、关闭的开销,提升响应速度,方便管理,常应用于线程池、连接池等

Executor框架将工作任务与执行(线程池)解耦分离,工作任务分为无返回值的Runnable和有返回值的Callable

Executor实际只处理Runnable任务,会将Callable任务封装成FutureTask适配Runnable执行

线程池使用工作队列来管理线程,线程执行完任务会从阻塞队列取任务执行,当非核心线程空闲一定时间后会被关闭

线程池执行时,如果工作队列线程数量小于核心线程数,则创建线程来执行(相当预热)

如果工作队列线程数量大于核心线程数量,并且阻塞队列未满则放入阻塞队列

如果阻塞队列已满,还未达到最大线程数量则创建非核心线程执行任务

如果已达到最大线程数量则使用拒绝策略

配置参数CPU密集型为CPU核数+1;IO密集型为2倍CPU核数;具体配置需要测试

处理异常可以直接捕获任务,Callable可以捕获get,也可以继承线程池实现afterExecutor记录异常,还可以在创建线程时就设置处理未捕获异常方法

处理定时任务的线程池由延迟队列实现,时间越短的定时任务越先执行,线程会从延迟队列中获取定时任务(时间已到的情况),时间未到就等待

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

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

相关文章

小节1:Python字符串打印

1、字符串拼接 用可以将两个字符串拼接成一个字符串 print("你好 " "这是一串代码") 输出&#xff1a; 2、单双引号转义 当打印的字符串中带有引号或双引号时&#xff0c;使用\或\"表示 print("He said \"Let\s go!\"") 输…

OpenRoads两期工程量(挖填)方量计算

分别生成各期地形&#xff0c;由Create Terrain Model From Ascii File展点&#xff1b; 其中至少一期地形特征设为Existing组特征 创建挖填体积&#xff08;Terrain\Analysis\Create Cut Fill Volumes或Home\Model Analysis and Reporting\Civil Analysis\Create Cut Fill Vo…

深度学习相关VO梳理

相关论文 基于学习的VO 相关&#xff1a; DeepVO Towards End-to-End Visual Odometry with Deep Recurrent Convolutional Neural Networks&#xff08;ICRA&#xff0c;2017&#xff09; TartanVO: A Generalizable Learning-based VO(CoRL2021) SimVODIS: Simultaneous Vis…

第22章 自旋锁死锁实验(iTOP-RK3568开发板驱动开发指南 )

在上一小节中&#xff0c;学习了内核中自旋锁的使用&#xff0c;而自旋锁若是使用不当就会产生死锁&#xff0c;在本章将会对自旋锁的特殊情况-死锁进行讲解。 22.1 自旋锁死锁 死锁是指两个或多个事物在同一资源上相互占用&#xff0c;并请求锁定对方的资源&#xff0c;从而…

【Docker】容器化应用程序的配置管理策略与实践

一、引言 1.1 Docker的背景和优势 Docker是一种开源的容器化平台&#xff0c;简化应用程序的打包、交付和运行过程。基于Linux容器技术&#xff0c;通过提供一个轻量级、可移植和自包含的容器来实现应用程序的隔离和部署。 在传统的应用程序开发和部署中&#xff0c;往往需要…

九科-模块化-创建目录_如果不存在

python代码 # 创建目录_如果不存在 def create_directory_if_not_exists(dir_path):# 如果目录不存在if not os.path.exists(dir_path):# 创建目录os.makedirs(dir_path) 九科组件模块 总图 查询目录是否存在 IF判断目录是否存在 如果目录不存在&#xff0c;创建目录

syn洪流原理

TCP三次握手 建立连接发送或回应第一次握手客户端发送报文&#xff0c;标志位为SYN&#xff08;seqa&#xff09;第二次握手服务器发送报文&#xff0c;标志位为SYN&#xff0c;ACK&#xff08;seqb,acka1&#xff09;第三次握手客户端回应服务器报文&#xff0c;标志位为ACK&…

Java 设置免登录请求接口被拦截问题

1、在设置免登录时&#xff0c;前端将请求的路由添加到白名单后&#xff0c;请求接口还是被拦截到了&#xff0c;将请求接口也设置后还是会被拦截跳转到登录页面 通过JAVA 注解 Anonymous 进行设置匿名访问就可以了

Docker 的常用命令

0 基本命令 概述 [root192 home]# docker --helpUsage: docker [OPTIONS] COMMANDA self-sufficient runtime for containersOptions:--config string Location of client configfiles (default "/root/.docker")-c, --context string Name of the context…

js中如何判断一个对象是否为空对象?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 使用 Object.keys()⭐ 使用 for...in 循环⭐ 使用 JSON.stringify()⭐ 使用 ES6 的 Object.getOwnPropertyNames()⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带…

windows本地认证

来自帅气学弟得经验 阅读流程 windows系统认证包括**本地交互式认证 **和 网络认证 交互式登录&#xff1a;向本地计算机或域账户确认用户的身份 网络登录&#xff1a;对用户尝试访问的网络服务或资源提供用户认证 若是本地用户本地认证需要了解**windows密码 **&#xff0c;…

【C++心愿便利店】No.5---构造函数和析构函数

文章目录 前言一、类的6个默认成员函数二、构造函数三、析构函数 前言 &#x1f467;个人主页&#xff1a;小沈YO. &#x1f61a;小编介绍&#xff1a;欢迎来到我的乱七八糟小星球&#x1f31d; &#x1f4cb;专栏&#xff1a;C 心愿便利店 &#x1f511;本章内容&#xff1a;类…

通过curl命令分析http接口请求各阶段的耗时等

目录 一、介绍二、功能1、-v 输出请求 响应头状态码 响应文本等信息2、-x 测试代理ip是否能在该网站使用3、-w 额外输出查看接口请求响应的消耗时间4、-o 将响应结果存储到文件里面5、-X post请求测试 (没测成功用的不多) 一、介绍 Curl是一个用于发送和接收请求的命令行工具和…

TCP/IP传输协议学习

了解完整的通信过程 1.发送方源终端设备的应用创建数据。 2.当数据在源终端设备中沿协议栈向下传递&#xff0c;对其分段和封装。 3.在协议栈网络接入层的介质上生成数据。 4.通过由介质和任意中间设备组成的网际层网络传输数据。 5.在目的终端设备中沿协议栈向上传递时对其…

用队列实现栈(C语言版本)

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

ArcGIS 10.5安装教程!

软件介绍&#xff1a; ArcGIS Desktop 10.5中文特别版是一款功能强大的GSI专业电子地图信息编辑和开发软件&#xff0c;ArcGIS Desktop 包括两种可实现制图和可视化的主要应用程序&#xff0c;即 ArcMap 和 ArcGIS Pro。ArcMap 是用于在 ArcGIS Desktop 中进行制图、编辑、分析…

滴滴:二季度中国出行营收同比增长57%,6月日均单量超3000万单

9月9日&#xff0c;滴滴在其官网发布2023年第二季度业绩报告&#xff0c;二季度滴滴实现总收入488亿元&#xff0c;同比增长52.6%&#xff1b;归属于滴滴普通股股东的净亏损为3亿元&#xff0c;经调整EBITA亏损1000万元。 分业务来看&#xff0c;二季度滴滴中国出行&#xff0…

北京运营《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书

北京运营《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书

当所有行业都在数字化转型时,企咨行业如何快速“破局”

党的二十大报告指出&#xff0c;“加快发展数字经济&#xff0c;促进数字经济和实体经济深度融合&#xff0c;打造具有国际竞争力的数字产业集群。”随着新一轮科技革命和产业变革深入发展&#xff0c;数字化转型已经不是一道“选择题”&#xff0c;而是一堂“必修课”。 数字…

国产触控笔哪个牌子好?开学平价好用触控笔排行榜

很多学生在新学期的时候&#xff0c;都会用到电容笔&#xff0c;这说明电容笔的重要性。苹果推出了ipad专用的电容笔后&#xff0c;这种电容笔便成为了市场上最热门的产品&#xff0c;只是因为Apple Pencil的价格过于昂贵&#xff0c;所以很多人并没有购买得起。所以&#xff0…