SpringBoot 最大连接数及最大并发数是多少?图解就看到了!

在SpringBoot2.7.10版本中内置Tomcat版本是9.0.73,SpringBoot内置Tomcat的默认设置如下:

  • Tomcat的连接等待队列长度,默认是100

  • Tomcat的最大连接数,默认是8192

  • Tomcat的最小工作线程数,默认是10

  • Tomcat的最大线程数,默认是200

  • Tomcat的连接超时时间,默认是20s

图片

相关配置及默认值如下


   
   
  1. server:
  2.   tomcat:
  3.     # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
  4.     accept-count:  100
  5.     # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
  6.     max-connections:  8192
  7.     threads:
  8.       # 工作线程的最小数量,初始化时创建的线程数
  9.       min-spare:  10
  10.       # 工作线程的最大数量 io密集型建议 10倍的cpu数,cpu密集型建议cpu数 + 1,绝大部分应用都是io密集型
  11.       max:  200
  12.     # 连接器在接受连接后等待显示请求 URI 行的时间。
  13.     connection-timeout:  20000
  14.     # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 - 1 时不会超时。
  15.     keep-alive-timeout:  20000
  16.     # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为 01时,禁用keep-alive和流水线处理。当设置为- 1时,允许无限数量的流水线处理或keep-alive请求。 
  17.     max-keep-alive-requests:  100

2架构图

图片

当连接数大于maxConnections+acceptCount + 1时,新来的请求不会收到服务器拒绝连接响应,而是不会和新的请求进行3次握手建立连接,一段时间后(客户端的超时时间或者Tomcat的20s后)会出现请求连接超时。

3TCP的3次握手4次挥手

图片

4时序图

图片

5核心参数

AcceptCount

全连接队列容量,等同于backlog参数,与Linux中的系统参数somaxconn取较小值,Windows中没有系统参数。

NioEndpoint.java

   
   
  1. serverSock  = ServerSocketChannel. open();
  2. socketProperties.setProperties(serverSock.socket());
  3. InetSocketAddress addr  = new InetSocketAddress(getAddress(), getPortWithOffset());
  4. / / 这里
  5. serverSock.socket().bind(addr,getAcceptCount());
MaxConnections

Acccptor.java


   
   
  1. / / 线程的 run方法。
  2. public void  run() {    
  3.   while (!stopCalled) { 
  4.        / / 如果我们已达到最大连接数,等待
  5.          connectionLimitLatch.countUpOrAwait();
  6.              / / 接受来自服务器套接字的下一个传入连接
  7.             socket  = endpoint.serverSocketAccept()
  8.              / / socket. close 释放的时候 调用 connectionLimitLatch.countDown();       
MinSpareThread/MaxThread

AbstractEndpoint.java


   
   
  1. / / tomcat 启动时
  2. public void createExecutor() {
  3.         internalExecutor  =  true;
  4.       / / 容量为Integer.MAX_ VALUE
  5.         TaskQueue taskqueue  = new TaskQueue();
  6.         TaskThreadFactory tf  = new TaskThreadFactory(getName()  +  "-exec-", daemon, getThreadPriority());
  7.       / / Tomcat扩展的线程池
  8.         executor  = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(),  60, TimeUnit.SECONDS,taskqueue, tf);
  9.         taskqueue.setParent( (ThreadPoolExecutor) executor);
  10. }

重点重点重点

Tomcat扩展了线程池增强了功能。

  • JDK线程池流程:minThreads --> queue --> maxThreads --> Exception

  • Tomcat增强后: minThreads --> maxThreads --> queue --> Exception

MaxKeepAliveRequests

长连接,在发送了maxKeepAliveRequests个请求后就会被服务器端主动断开连接。

在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。

较大的 MaxKeepAliveRequests 值可能会导致服务器上的连接资源被长时间占用。根据您的具体需求,您可以根据服务器的负载和资源配置来调整 MaxKeepAliveRequests 的值,以平衡并发连接和服务器资源的利用率。


   
   
  1. NioEndpoint.setSocketOptions 
  2.  socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
  3. Http 11Processor.service(SocketWrapperBase <? > socketWrapper)
  4.   keepAlive  =  true;
  5.   while(!getErrorState().isError()  & & keepAlive  & & !isAsync()  & & upgradeToken  = =  null  & &
  6.                 sendfileState  = = SendfileState.DONE  & & !protocol.isPaused()) {
  7.      / / 默认 100  
  8.  int maxKeepAliveRequests  = protocol.getMaxKeepAliveRequests();
  9.   if (maxKeepAliveRequests  = =  1) {
  10.      keepAlive  =  false;
  11.  }  else  if (maxKeepAliveRequests  >  0  & &
  12.              / /    
  13.          socketWrapper.decrementKeepAlive()  <=  0) {
  14.      keepAlive  =  false;
  15.  }
ConnectionTimeout

连接的生存周期,当已经建立的连接,在connectionTimeout时间内,如果没有请求到来,服务端程序将会主动关闭该连接。

  • 在Tomcat 9中,ConnectionTimeout的默认值是20000毫秒,也就是20秒。

  • 如果该时间过长,服务器将要等待很长时间才会收到客户端的请求结果,从而导致服务效率低下。如果该时间过短,则可能会出现客户端在请求过程中网络慢等问题,而被服务器取消连接的情况。

  • 由于某个交换机或者路由器出现了问题,导致某些post大文件的请求堆积在交换机或者路由器上,tomcat的工作线程一直拿不到完整的文件数据。

NioEndpoint.Poller#run()


   
   
  1.   / / Check  for  read timeout
  2.   if ((socketWrapper.interestOps()  & SelectionKey.OP_ READ= = SelectionKey.OP_ READ) {
  3.      long delta  = now - socketWrapper.getLastRead();
  4.      long timeout  = socketWrapper.getReadTimeout();
  5.       if (timeout  >  0  & & delta  > timeout) {
  6.          readTimeout  =  true;
  7.      }
  8.  }
  9.   / / Check  for  write timeout
  10.   if (!readTimeout  & & (socketWrapper.interestOps()  & SelectionKey.OP_ WRITE= = SelectionKey.OP_ WRITE) {
  11.      long delta  = now - socketWrapper.getLastWrite();
  12.      long timeout  = socketWrapper.getWriteTimeout();
  13.       if (timeout  >  0  & & delta  > timeout) {
  14.          writeTimeout  =  true;
  15.      }
  16.  }
KeepAliveTimeout

等待另一个 HTTP 请求的时间,然后关闭连接。当未设置时,将使用 connectionTimeout。当设置为 -1 时,将没有超时。

Http11InputBuffer.parseRequestLine


   
   
  1. / /  Read new bytes  if needed
  2. if (byteBuffer.position()  >= byteBuffer. limit()) {
  3.      if (keptAlive) {
  4.          / / 还没有读取任何请求数据,所以使用保持活动超时
  5.         wrapper.setReadTimeout(keepAliveTimeout);
  6.     }
  7.      if (!fill( false)) {
  8.          / / A  read  is pending, so  no longer  in  initial state
  9.         parsingRequestLinePhase  =  1;
  10.          return  false;
  11.     }
  12.      / /  至少已收到请求的一个字节 切换到套接字超时。
  13.      wrapper.setReadTimeout(connectionTimeout);
  14. }

6内部线程

Acceptor

Acceptor:接收器,作用是接受scoket网络请求,并调用setSocketOptions()封装成为NioSocketWrapper,并注册到Poller的events中。注意查看run方法org.apache.tomcat.util.net.Acceptor#run


   
   
  1. public void run() {
  2.     while (!stopCalled) {
  3.         // 等待下一个请求进来
  4.        socket = endpoint.serverSocketAccept();
  5.          // 注册socket到Poller,生成PollerEvent事件
  6.        endpoint.setSocketOptions(socket);
  7.            // 向轮询器注册新创建的套接字
  8.                 - poller.register(socketWrapper);
  9.                     - (SynchronizedQueue( 128))events. add( new PollerEvent(socketWrapper))  
Poller

Poller:轮询器,轮询是否有事件达到,有请求事件到达后,以NIO的处理方式,查询Selector取出所有请求,遍历每个请求的需求,分配给Executor线程池执行。查看org.apache.tomcat.util.net.NioEndpoint.Poller#run()


   
   
  1. public void  run() {
  2.    while ( true) {
  3.             / /查询selector取出所有请求事件
  4.            Iterator <SelectionKey > iterator  =
  5.                keyCount  >  0 ? selector.selectedKeys().iterator() :  null;
  6.             / / 遍历就绪键的集合并调度任何活动事件。
  7.            while (iterator ! =  null  & & iterator.hasNext()) {
  8.                SelectionKey sk  = iterator. next();
  9.                iterator.remove();
  10.                NioSocketWrapper socketWrapper  = (NioSocketWrapper) sk.attachment();
  11.                 / / 分配给Executor线程池执行处理请求 key
  12.                 if (socketWrapper ! =  null) {
  13.                    processKey(sk, socketWrapper);
  14.                    - processSocket(socketWrapper, SocketEvent. OPEN_ READ /SocketEvent. OPEN_ WRITE)
  15.                        - executor.execute((Runnable)new SocketProcessor(socketWrapper,SocketEvent))
  16.                }
  17.            }
TomcatThreadPoolExecutor

真正执行连接读写操作的线程池,在JDK线程池的基础上进行了扩展优化。

AbstractEndpoint.java


   
   
  1. public  void  createExecutor () {
  2.     internalExecutor =  true;
  3.      TaskQueue  taskqueue  =  new  TaskQueue();
  4.      TaskThreadFactory  tf  =  new  TaskThreadFactory(getName() +  "-exec-", daemon, getThreadPriority());
  5.   // tomcat自定义线程池
  6.     executor =  new  ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(),  60, TimeUnit.SECONDS,taskqueue, tf);
  7.     taskqueue.setParent( (ThreadPoolExecutor) executor);
  8. }

TomcatThreadPoolExecutor.java


   
   
  1. // 与 java.util.concurrent.ThreadPoolExecutor 相同,但实现了更高效的getSubmittedCount()方法,用于正确处理工作队列。
  2. // 如果未指定 RejectedExecutionHandler,将配置一个默认的,并且该处理程序将始终抛出 RejectedExecutionException
  3. public  class  ThreadPoolExecutor  extends  java.util.concurrent.ThreadPoolExecutor {
  4.   // 已提交但尚未完成的任务数。这包括队列中的任务和已交给工作线程但后者尚未开始执行任务的任务。
  5.      // 这个数字总是大于或等于getActiveCount() 。
  6.      private  final  AtomicInteger  submittedCount  =  new  AtomicInteger( 0);
  7.     
  8.      @Override
  9.      protected  void  afterExecute (Runnable r, Throwable t) {
  10.          if (!(t  instanceof StopPooledThreadException)) {
  11.             submittedCount.decrementAndGet();
  12.         }
  13.      @Override
  14.      public  void  execute (Runnable command){
  15.          // 提交任务的数量+1
  16.         submittedCount.incrementAndGet();
  17.          try {
  18.              //  线程池内部方法,真正执行的方法。就是JDK线程池原生的方法。
  19.              super.execute(command);
  20.         }  catch (RejectedExecutionException rx) {
  21.              // 再次把被拒绝的任务放入到队列中。
  22.              if ( super.getQueue()  instanceof TaskQueue) {
  23.                  final  TaskQueue  queue  = (TaskQueue) super.getQueue();
  24.                  try {
  25.                        //强制的将任务放入到阻塞队列中
  26.                      if (!queue.force(command, timeout, unit)) {
  27.                          //放入失败,则继续抛出异常
  28.                         submittedCount.decrementAndGet();
  29.                          throw  new  RejectedExecutionException(sm.getString( "threadPoolExecutor.queueFull"));
  30.                     }
  31.                 }  catch (InterruptedException x) {
  32.                       //被中断也抛出异常
  33.                     submittedCount.decrementAndGet();
  34.                      throw  new  RejectedExecutionException(x);
  35.                 }
  36.             }  else {
  37.                   //不是这种队列,那么当任务满了之后,直接抛出去。
  38.                 submittedCount.decrementAndGet();
  39.                  throw rx;
  40.             }
  41.         }
  42.     }

   
   
  1. / **
  2.   * 实现Tomcat特有逻辑的自定义队列
  3.   * /
  4. public  class TaskQueue extends LinkedBlockingQueue <Runnable > {
  5.     private static  final long serialVersionUID  =  1L;
  6.     private transient volatile ThreadPoolExecutor parent  =  null;
  7.     private static  final int  DEFAULT_FORCED_REMAINING_CAPACITY  = - 1;
  8.      / **
  9.       * 强制遗留的容量
  10.       * /
  11.     private int forcedRemainingCapacity  = - 1;
  12.      / **
  13.       * 队列的构建方法
  14.       * /
  15.     public TaskQueue() {
  16.     }
  17.     public TaskQueue(int capacity) {
  18.          super(capacity);
  19.     }
  20.     public TaskQueue(Collection <? extends Runnable > c) {
  21.          super(c);
  22.     }
  23.      / **
  24.       * 设置核心变量
  25.       * /
  26.     public void setParent(ThreadPoolExecutor parent) {
  27.         this.parent  = parent;
  28.     }
  29.      / **
  30.       * put:向阻塞队列填充元素,当阻塞队列满了之后,put时会被阻塞。
  31.       * offer:向阻塞队列填充元素,当阻塞队列满了之后,offer会返回 false
  32.       *
  33.       * @param o 当任务被拒绝后,继续强制的放入到线程池中
  34.       * @ return 向阻塞队列塞任务,当阻塞队列满了之后,offer会返回 false
  35.       * /
  36.     public  boolean force(Runnable o) {
  37.          if (parent  = =  null || parent.isShutdown()) {
  38.             throw new RejectedExecutionException( "taskQueue.notRunning");
  39.         }
  40.          return  super.offer(o);
  41.     }
  42.      / **
  43.       * 带有阻塞时间的塞任务
  44.       * /
  45.     @Deprecated
  46.     public  boolean force(Runnable o, long timeout, TimeUnit  unit) throws InterruptedException {
  47.          if (parent  = =  null || parent.isShutdown()) {
  48.             throw new RejectedExecutionException( "taskQueue.notRunning");
  49.         }
  50.          return  super.offer(o, timeout,  unit);  / /forces the item onto the queue,  to be used  if the task  is rejected
  51.     }
  52.      / **
  53.       * 当线程真正不够用时,优先是开启线程(直至最大线程),其次才是向队列填充任务。
  54.       *
  55.       * @param runnable 任务
  56.       * @ return  false 表示向队列中添加任务失败,
  57.       * /
  58.     @ Override
  59.     public  boolean offer(Runnable runnable) {
  60.          if (parent  = =  null) {
  61.              return  super.offer(runnable);
  62.         }
  63.          / /若是达到最大线程数,进队列。
  64.          if (parent.getPoolSize()  = = parent.getMaximumPoolSize()) {
  65.              return  super.offer(runnable);
  66.         }
  67.          / /当前活跃线程为 10个,但是只有 8个任务在执行,于是,直接进队列。
  68.          if (parent.getSubmittedCount()  < (parent.getPoolSize())) {
  69.              return  super.offer(runnable);
  70.         }
  71.          / /当前线程数小于最大线程数,那么直接返回 false,去创建最大线程
  72.          if (parent.getPoolSize()  < parent.getMaximumPoolSize()) {
  73.              return  false;
  74.         }
  75.          / /否则的话,将任务放入到队列中
  76.          return  super.offer(runnable);
  77.     }
  78.      / **
  79.       * 获取任务
  80.       * /
  81.     @ Override
  82.     public Runnable poll(long timeout, TimeUnit  unit) throws InterruptedException {
  83.         Runnable runnable  =  super.poll(timeout,  unit);
  84.          / /取任务超时,会停止当前线程,来避免内存泄露
  85.          if (runnable  = =  null  & & parent ! =  null) {
  86.             parent.stopCurrentThreadIfNeeded();
  87.         }
  88.          return runnable;
  89.     }
  90.      / **
  91.       * 阻塞式的获取任务,可能返回 null
  92.       * /
  93.     @ Override
  94.     public Runnable take() throws InterruptedException {
  95.          / /当前线程应当被终止的情况下:
  96.          if (parent ! =  null  & & parent.currentThreadShouldBeStopped()) {
  97.             long keepAliveTime  = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);
  98.              return poll(keepAliveTime, TimeUnit.MILLISECONDS);
  99.         }
  100.          return  super.take();
  101.     }
  102.      / **
  103.       * 返回队列的剩余容量
  104.       * /
  105.     @ Override
  106.     public int remainingCapacity() {
  107.          if (forcedRemainingCapacity  >  DEFAULT_FORCED_REMAINING_CAPACITY) {
  108.              return forcedRemainingCapacity;
  109.         }
  110.          return  super.remainingCapacity();
  111.     }
  112.      / **
  113.       * 强制设置剩余容量
  114.       * /
  115.     public void setForcedRemainingCapacity(int forcedRemainingCapacity) {
  116.         this.forcedRemainingCapacity  = forcedRemainingCapacity;
  117.     }
  118.      / **
  119.       * 重置剩余容量
  120.       * /
  121.     void resetForcedRemainingCapacity() {
  122.         this.forcedRemainingCapacity  =  DEFAULT_FORCED_REMAINING_CAPACITY;
  123.     }

   
   
  1. / **
  2.   * 实现Tomcat特有逻辑的自定义队列
  3.   * /
  4. public  class TaskQueue extends LinkedBlockingQueue <Runnable > {
  5.     private static  final long serialVersionUID  =  1L;
  6.     private transient volatile ThreadPoolExecutor parent  =  null;
  7.     private static  final int  DEFAULT_FORCED_REMAINING_CAPACITY  = - 1;
  8.      / **
  9.       * 强制遗留的容量
  10.       * /
  11.     private int forcedRemainingCapacity  = - 1;
  12.      / **
  13.       * 队列的构建方法
  14.       * /
  15.     public TaskQueue() {
  16.     }
  17.     public TaskQueue(int capacity) {
  18.          super(capacity);
  19.     }
  20.     public TaskQueue(Collection <? extends Runnable > c) {
  21.          super(c);
  22.     }
  23.      / **
  24.       * 设置核心变量
  25.       * /
  26.     public void setParent(ThreadPoolExecutor parent) {
  27.         this.parent  = parent;
  28.     }
  29.      / **
  30.       * put:向阻塞队列填充元素,当阻塞队列满了之后,put时会被阻塞。
  31.       * offer:向阻塞队列填充元素,当阻塞队列满了之后,offer会返回 false
  32.       *
  33.       * @param o 当任务被拒绝后,继续强制的放入到线程池中
  34.       * @ return 向阻塞队列塞任务,当阻塞队列满了之后,offer会返回 false
  35.       * /
  36.     public  boolean force(Runnable o) {
  37.          if (parent  = =  null || parent.isShutdown()) {
  38.             throw new RejectedExecutionException( "taskQueue.notRunning");
  39.         }
  40.          return  super.offer(o);
  41.     }
  42.      / **
  43.       * 带有阻塞时间的塞任务
  44.       * /
  45.     @Deprecated
  46.     public  boolean force(Runnable o, long timeout, TimeUnit  unit) throws InterruptedException {
  47.          if (parent  = =  null || parent.isShutdown()) {
  48.             throw new RejectedExecutionException( "taskQueue.notRunning");
  49.         }
  50.          return  super.offer(o, timeout,  unit);  / /forces the item onto the queue,  to be used  if the task  is rejected
  51.     }
  52.      / **
  53.       * 当线程真正不够用时,优先是开启线程(直至最大线程),其次才是向队列填充任务。
  54.       *
  55.       * @param runnable 任务
  56.       * @ return  false 表示向队列中添加任务失败,
  57.       * /
  58.     @ Override
  59.     public  boolean offer(Runnable runnable) {
  60.          if (parent  = =  null) {
  61.              return  super.offer(runnable);
  62.         }
  63.          / /若是达到最大线程数,进队列。
  64.          if (parent.getPoolSize()  = = parent.getMaximumPoolSize()) {
  65.              return  super.offer(runnable);
  66.         }
  67.          / /当前活跃线程为 10个,但是只有 8个任务在执行,于是,直接进队列。
  68.          if (parent.getSubmittedCount()  < (parent.getPoolSize())) {
  69.              return  super.offer(runnable);
  70.         }
  71.          / /当前线程数小于最大线程数,那么直接返回 false,去创建最大线程
  72.          if (parent.getPoolSize()  < parent.getMaximumPoolSize()) {
  73.              return  false;
  74.         }
  75.          / /否则的话,将任务放入到队列中
  76.          return  super.offer(runnable);
  77.     }
  78.      / **
  79.       * 获取任务
  80.       * /
  81.     @ Override
  82.     public Runnable poll(long timeout, TimeUnit  unit) throws InterruptedException {
  83.         Runnable runnable  =  super.poll(timeout,  unit);
  84.          / /取任务超时,会停止当前线程,来避免内存泄露
  85.          if (runnable  = =  null  & & parent ! =  null) {
  86.             parent.stopCurrentThreadIfNeeded();
  87.         }
  88.          return runnable;
  89.     }
  90.      / **
  91.       * 阻塞式的获取任务,可能返回 null
  92.       * /
  93.     @ Override
  94.     public Runnable take() throws InterruptedException {
  95.          / /当前线程应当被终止的情况下:
  96.          if (parent ! =  null  & & parent.currentThreadShouldBeStopped()) {
  97.             long keepAliveTime  = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);
  98.              return poll(keepAliveTime, TimeUnit.MILLISECONDS);
  99.         }
  100.          return  super.take();
  101.     }
  102.      / **
  103.       * 返回队列的剩余容量
  104.       * /
  105.     @ Override
  106.     public int remainingCapacity() {
  107.          if (forcedRemainingCapacity  >  DEFAULT_FORCED_REMAINING_CAPACITY) {
  108.              return forcedRemainingCapacity;
  109.         }
  110.          return  super.remainingCapacity();
  111.     }
  112.      / **
  113.       * 强制设置剩余容量
  114.       * /
  115.     public void setForcedRemainingCapacity(int forcedRemainingCapacity) {
  116.         this.forcedRemainingCapacity  = forcedRemainingCapacity;
  117.     }
  118.      / **
  119.       * 重置剩余容量
  120.       * /
  121.     void resetForcedRemainingCapacity() {
  122.         this.forcedRemainingCapacity  =  DEFAULT_FORCED_REMAINING_CAPACITY;
  123.     }

JDK线程池架构图

图片

Tomcat线程架构

图片

7测试

如下配置举例


   
   
  1. server:
  2.   port:  8080
  3.   tomcat:
  4.     accept-count:  3
  5.     max-connections:  6
  6.     threads:
  7.       min-spare:  2
  8.       max:  3

使用ss -nlt查看全连接队列容量。


   
   
  1. ss -nltp
  2. ss -nlt|grep 8080
  3. - Recv-Q表示(acceptCount)全连接队列目前长度
  4. - Send-Q表示(acceptCount)全连接队列的容量。

静默状态

图片

6个并发连接

结果同上

9个并发连接

图片

10个并发连接

图片

11个并发连接

结果同上

使用ss -nt查看连接状态。


   
   
  1. ss -ntp
  2. ss -nt|grep 8080
  3. - Recv-Q表示客户端有多少个字节发送但还没有被服务端接收
  4. - Send-Q就表示为有多少个字节未被客户端接收。

静默状态

图片

6个并发连接

图片

9个并发连接

图片

补充个netstat

图片

10个并发连接

结果同上,队列中多加了个

11个并发连接

图片

超出连接后,会有个连接一直停留在SYN_RECV状态,不会完成3次握手了。

超出连接后客户端一直就停留在SYN-SENT状态,服务端不会再发送SYN+ACK,直到客户端超时(20s内核控制)断开。

客户端请求超时(需要等待一定时间(20s))。

这里如果客户端设置了超时时间,要和服务端3次握手超时时间对比小的为准。

12个并发连接

图片

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值