基于源码详解ThreadPoolExecutor实现原理

news2025/2/26 3:03:29

个人博客地址

基于源码详解ThreadPoolExecutor实现原理 | iwts’s blog

内容拆分

这里算是一个总集,内容太多,拆分成几个比较重要的小的模块:

ThreadPoolExecutor基于ctl变量的声明周期管理 | iwts’s blog

ThreadPoolExecutor 工作线程Worker自身锁设计 | iwts’s blog

ThreadPoolExecutor 线程回收时机详解 | iwts’s blog

Java ThreadPoolExecutor

线程池,Thread Pool,是一种基于池化思想管理线程的工具,一般是多线程服务器中使用很多,例如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。

线程池维护多个线程,等待监督管理者分配可并发执行的任务。从而避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

  1. 降低资源消耗:重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,允许任务延期执行或定期执行。

ThreadPoolExecutor核心设计

线程池顶级接口Executor

顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

简单粗暴,执行就完了。

线程池扩充服务接口ExecutorService

ExecutorService接口增加了一些能力:

  1. 扩充执行任务的能力,例如submit(),可以为一个或一批异步任务生成Future。

  2. 提供了管控线程池的方法,例如shutdown(),停止线程池的运行。

    a. 包括状态监控,例如isShutdown()

  3. 执行流程方法,例如invokeAll()

线程池抽象类AbstractExecutorService

将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

ThreadPoolExecutor实现

最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor运行流程

线程池在内部构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

线程池的运行主要分成两部分:

  1. 任务管理。
  2. 线程管理。

任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

  1. 直接申请线程执行该任务。
  2. 缓冲到队列中等待线程执行。
  3. 拒绝该任务。

线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

线程池生命周期管理-ctl字段的应用

线程池内部使用一个变量ctl维护两个值:

  1. 运行状态(runState)
  2. 线程数量 (workerCount)

在具体实现中,就是进行位运算:

COUNT_BITS如果是32位的话,那么结合下面的一套左移、与、非的位运算,可以总结为:

  1. ctl的高3位保存runState,即运行状态。
  2. ctl的低29位保存workerCount,即有效线程数量。

除了ctl解析方法,还提供ctl计算方法,即根据runState和workerCount,计算出ctl值。

这样合并的好处是,操作的时候单锁就可以处理了(CAS也非常方便),位运算速度也快。

线程池运行状态

运行状态运行状态desc
running运行能接收新任务,能处理队列任务
shutdown关闭不接受新任务,能处理队列任务
stop停止不接受新任务,不处理队列任务,中断处理任务的线程
tidying整理所有任务已结束,workerCount=0
terminatedterminated()方法执行后进入该状态

线程池工作流程

任务提交-submit()

就是一个入口方法,但是分为阻塞和非阻塞:

  1. submit:返回Future对象,底层仍然是execute,Future操作时可能会阻塞。
  2. execute:常规非阻塞方法,提交后正常执行。

submit是异步编程时可能会用到:

本质上是利用Future,还是普通的调用execute执行,对外透出Future对象。

任务调度-execute()

任务调度是线程池的核心,当用户提交了一个任务,接下来这个任务将如何执行都是由任务调度来决定。核心入口方法:java.util.concurrent.ThreadPoolExecutor#execute

代码也并不难:

其中reject()方法是拒绝策略,具体参考下面的内容,addWorker()下面工作线程部分详讲,目前需要知道是创建了一个工作线程,入参有Runable对象则说明创建后就用这个线程来执行该对象的任务方法,没有就说明只是创建线程。

总的流程可以参考:

如果从大的方向看,整个execute的调度工作为:

简而言之:

  1. 当前线程数量小于核心池数量,创建线程并执行。
  2. 当前线程数量大于核心池数量,且任务队列不满,加入任务队列。
  3. 如果任务队列已经满了,但是线程池数量小于线程池设定的最大数量,创建一个线程来执行任务。
  4. 如果比最大数量都大,只能拒绝服务。

任务缓冲模块设计

任务缓冲模块是线程池能够管理任务的核心。

线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。

线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。

阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

任务阻塞队列

缓冲模块的核心设计了,JUC提供了阻塞队列框架:BlockingQueue,设计思想如下:

而具体的实现则交给使用者自行选择,比较简单的,直接从JUC包的实现中选择阻塞队列实现类就可以了。

JUC原生阻塞队列类型对比

不同阻塞队列的具体实现类有不同的特性,简单总结如下:

名称描述
ArrayBlockingQueue底层数据结构为数组,队列大小要主动设置,FIFO。
LinkedBlockingQueue底层数据结构为单向链表,队列大小为int最大位,FIFO。
PriorityBlockingQueue底层数据结构为数组,无界,按存放对象compareTo指定优先级进出。
DelayQueue基于PriorityBlockingQueue,指定延期时间,延期时间到后从队列中获取数据。
SynchronousQueue不存储数据,只有在执行take的时候,才会执行put。
LinkedTransferQueue跟LinkedBlockingQueue类似,多了transfer相关方法。
LinkedBlockingDeque跟LinkedBlockingQueue类似,底层改成双向链表,组成双向阻塞队列,高并发时,锁竞争最多减少一半。

任务申请-getTask()

注意不是任务提交。

根据上面任务调度的内容,正常情况下,任务提交后的执行有两种方式:

  1. 直接创建新的线程,并去执行任务。
  2. 线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务。

一般来说,对于业务请求量较大的系统,大部分情况下都是2,不会额外创建线程。都是丢入队列就完事。

1的执行时机其实是业务请求的时候,调用submit()或者execute()。那2的执行时机是由线程池保证的。这一块下面工作线程部分会讲到。

那么从任务队列中挑选一个任务并执行。

这个申请的核心代码为:getTask()

代码稍微有点长,分成两批解析:

常规队列维护,通过take()/poll()方法,阻塞/限时阻塞来获取任务。

任务拒绝-reject()

任务拒绝模块是线程池的保护部分,参考上面的代码,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,可以采取任务拒绝策略,以保护线程池。

核心执行方法为:reject()

所以拒绝策略需要实现一个接口RejectedExecutionHandler,其设计如下:

核心是要实现rejectedExecution()方法。

除了我们自定义,JUC也提供了一些常用拒绝策略实现类,对比参考为:

对象名称描述
AbortPolicy丢弃策略丢弃任务 & 抛出异常。线程池默认拒绝策略。
DiscardPolicy丢弃策略丢弃任务,但是不抛出异常。
DiscardOldestPolicy丢弃最老任务策略丢弃队列最前面的任务(最老),然后重新提交被拒绝的任务。
CallerRunsPolicy由调用线程来执行任务直接阻塞,由提交任务的线程自己来执行该任务。

Worker-工作线程管理

线程池设计了内部类Worker,主要是用来管理新建的线程,除了监控,核心的方法是:

  1. 执行。
  2. 申请任务。

此外还包括回收等线程监控类型方法。

由于一个工作线程对象,其中有一个具体的线程,那么本质上是不需要加锁的。竞争资源是任务队列,而任务队列由阻塞队列来实现。

可以看Worker的设计:

线程的创建-addWorker()

结合上面内容,任务在提交的时候,就是线程创建的时机,即核心方法:addWorker()

代码就不放了,主要是工作队列的维护。

需要注意的一点:addWorker()本身是不处理任何任务的。上面的流程图也可以看到,只截了一半,因为addWorker()本身只是新建一个工作线程,并不执行任何任务。

但是,其中的线程被创建后,会在addWorker()方法中start,开启Worker的真正的执行方法:run方法。

在工作队列全部维护结束后,start()方法开启任务,workerStarted=true,宣告工作线程真正执行起来。

线程工厂ThreadFactory

这里也是ThreadPoolExecutor的构造方法了,也可以看到上面的代码,一个线程的创建都是要走线程工厂的。

可以设定线程大量的数据。

一般是默认,可以看默认配置:

自定义的话,一般是设置一下daemon、线程name,或者有一些特殊操作。

目前个人用的不多。

工作线程的执行-runWorker()

上面可以知道,工作线程在创建之后,就直接开启任务开始执行了,那么Worker的run()方法就是工作线程核心执行方法,实际上就是:runWorker()

截图的后半部分,真正的执行。

核心方法是利用getTask()方法从工作队列中获取任务并执行。

那么这里看代码可以了解到firstTask的真正的意义:

等于说firstTask,就是当前工作线程的待执行任务。如果待执行任务为null,就执行任务申请方法获取任务,反之则正常执行。

支持提前设置,从而实现:先执行这个任务,再从任务队列中获取。

这个设计猜测是例如定时任务线程池,会获取任务后设置到firstTask,但是不执行,等待时间到了才执行。

后面的执行没啥好说的。细节一点的图:

线程生命周期管理

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。

线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。

可以看到,workers是HashSet,那么问题来了,线程池有大量的工作线程,频繁创建/清除线程的时候,用线程不安全的HashSet必然是有并发安全问题的。

所以线程池要求在操作workers的时候,都需要获锁,根据该锁对workers进行操作:

也就是说,在工作线程的创建/销毁,都要加上这个锁,例如工作线程的创建:

工作线程的回收

这里比较复杂,慢慢聊。

工作线程自身锁

Worker对象其实本身就是一把锁。这是个细节,Worker本身是实现了AQS的:

这里其实最主要的作用是工作线程的回收。虽然可以通过维护workers来完成对工作线程生命周期的管理,新建线程比较好理解,但是删除线程的时候,工作线程本身就是一种竞争资源了。回收的时候是可能恰好碰到调用的。

这里选择AQS的原因,其实可以看注释,这边简单翻译一部分:

Worker类存在的主要意义就是为了维护线程的中断状态。因为正在执行任务的线程是不应该被中断的。在线程真正开始运行任务之前,为了抑制中断。所以把 Worker 的状态初始化为负数-1。

完全看不懂,这里从其他角度慢慢绕过来解释一下。

线程的中断与回收

解释这个问题,首先看下Worker自身是从哪里调用锁的:

  1. 工作线程处理前后加锁。
  2. 工作线程尝试中断时尝试获锁。

第一个看代码runWorker()

也就是说,当前线程如果在处理,那么本身是给自己加锁的。
第二个看代码interruptIdleWorkers()

这里是不是就有点恍然大悟的意思了。

工作线程本身实现AQS,将自身当作竞争对象。

那么工作线程工作的时候,加锁,锁住自己,那么interruptIdleWorkers()方法在执行的时候,如果能获取锁,就说明一个问题:此时当前线程是没有在工作的。那么就会被中断掉。

为了实现这个功能,就只能选择不可重入锁,所以自己实现了AQS来实现这个特性。具体可以看代码实现。

基本可以推测到,interruptIdleWorkers()这个方法就是回收方法,那么其调用时机是什么?

线程回收时机
一个重要的回收时机-keepAliveTime

这里单独拉出来聊了,比较经典。

八股文一般说:keepAliveTime是线程存活时间,如果当前线程池线程数量大于核心池的时候,如果一个线程超过keepAliveTime没有获取到任务,则会触发线程回收。

这里聊聊相关源码。首先看基础的任务申请:

这里如果设置了超时时间的情况下,请求任务队列是调用的poll()方法,并指定了keepAliveTime。那么这个方法的意思就是,阻塞这么长时间,超过时间后直接返回null。

所以这里就对应到八股文了,如果此时poll()返回空,那么就是说当前队列里什么数据都没有,那么这里其实就是说明:该线程等待了keepAliveTime都没有获取到数据,也就是说这段时间全部是空闲。可以回收了。

而这里只是设置了timedOut标记,留给上层来处理:

这里判定之后返回个null。

直接跳出线程执行run()方法,在finally块中触发线程回收。processWorderExit()方法的底层就是下面的tryTerminate()了,会直接进行回收。

tryTerminate()

核心回收方法,根据其调用可以梳理出正常运行中的回收时机:

  1. 工作线程创建失败时:addWorkerFailed()

  2. runWorker()方法退出时。正常来说runWorker()方法是一个自旋,只有在任务申请失败时才会退出自旋。那么这个时机就是指任务队列已经清空了:

    总体流程为:

  3. shutdown()。可以看到执行了两次。

  4. shutdownNow()。

  5. remove(),移除任务时顺便执行一次。

  6. purge(),todo。

interruptIdleWorkers

一般是针对线程池本身参数进行操作的时候,会触发回收,看其调用方式,可以梳理出来全部的线程回收时机:

  1. shutdown()。
  2. 设置核心池大小的时候,如果当前线程池线程数量大于核心池数量大小,执行一次回收:
  3. 设置允许核心池超时时,执行一次回收:
  4. 设置最大池数量时,如果当前线程池线程数量大于最大池数量,执行一次回收:
  5. 设置线程池线程存活时间时,如果设置变小了,那么执行一次回收:
不使用ReentrantLock和synchronized的原因

跟上面的逻辑一样,为啥要加锁,就是为了区分线程是否中断,而ReentrantLock和synchronized都有一个重要特性:可重入。因为可重入,那么这个锁就没有意义了,因为线程都是一个,既然可重入那么就是必然能获锁了。

所以选用AQS,手动删掉可重入的特性,实现互斥。

tryAcquire实现不可重入

也可以看看重写AQS的获锁代码:

虽然设置的excluiveOwnerThread,但是完全不用,就是直接CAS获锁,没有重入的特性。

总结

综上,可以简单给出一个大致的回收流程:

而回收请求执行时机:

  1. 任务队列空。
  2. shutdown()等线程池关闭任务。
  3. 线程池变量更改的时候,例如核心池大小变更等。
  4. 任务移除。
  5. purge(),todo。

runWorker()方法的总结

也算是工作队列核心的总结吧:

线程池设置核心池和最大池的原因

个人理解了。

假设核心池数量为15,最大池数量为20。

线程的创建和销毁是很重的操作,所以线程池本意是想在核心池中的线程都能正常使用,偶尔使用最大池。

  1. 房子装修,一般来说聘15个工人,3天完成工作。
  2. 那么每天的工作量不一定,也有可能运水泥瓷砖的人不给力,导致今天工期延缓
  3. 但是不管怎样,3天左右或快或慢基本都能干完
  4. 运材料的人或多或少,每天都是15人份左右的装修材料。

那么就认为这15个工人是核心池,即完成任务基本就是需要这么些人,可以少点但不能多。

  1. 房子装修,这次设计的比较简单,13个人,3天完成工作。
  2. 那么多出来的两个人,要么找点别的活先干着。实在没活,就直接跑路了,也没啥好待的。

等于说核心池的线程释放。

  1. 房子装修,这次设计的比较复杂,15个人,干了一天后觉得不行,干不完了
  2. 老板为了不延期,说好的3天就3天,那么就临时加人手
  3. 花钱雇新工程师,让17个人,勉强3天干完。

等于说核心池线程不够用,则依旧可以创建线程。

  1. 老板也要挣钱,再让我快,我都不挣钱了,自然不会给你再增加工人,就这样凑合吧。

等于说达到了最大线程池。

那么keepAliveTime也是衍生出来的配置,控制线程空闲后多长时间后自动回收。

线程池数量设置方法论

这个跟业务相关,一般是跟着业务一起调整,包括实际上线之后的调整。刚开始可能也只是设计一个大概值。

本质上,一个任务可以区分成CPU密集型任务和I/O密集型任务。

  1. CPU密集型任务。

    1. 需要大量CPU计算。
      1. 所以大量的切换上下文非常影响性能,最好一个时间片内执行完毕。
    2. 那么这种情况下,如果线程数量给的太大,会导致CPU大量轮询,每个时间片给的也少。
    3. 所以CPU密集型任务尽量保证CPU核心数量和线程数量一致,减少上下文切换的损耗。
    4. 所以线程数量比CPU核心数量稍高或者相等相对会好一点。
  2. IO密集型任务(操作数据库也算IO)。

    1. 可能会阻塞。
    2. CPU可能会长时间空转,等待IO操作。
    3. 所以恰恰相反,这种任务需要设置很多线程,IO等待的时候不要让CPU闲着,去处理其他线程,提高吞吐量。
    4. 可以考虑线程数量是核心数量的两倍。
  3. 混合型任务。

    1. 这种就是取个中间值。
    2. 建议做好区分,分开处理。

美团遇到的问题

Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)

这篇文章有一些详细案例,可以看看。

线程池大小预估错误

比较常见,核心池最大池数量较小,从而导致触发拒绝策略。

但是不能太大,并发量太高可能会打挂下游。

线程池任务堆积

一方面是没有设置好量,容量太大,导致大量任务堆积,或者没有预估到任务RT可能会很高,瞬间就大量堆积。

而此时如果业务本身要求RT较低,那么整个方法接口就会因为堆积,持续发生大量超时。

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

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

相关文章

【PL理论深化】(13) 变量与环境:文法结构 | 真假表达式:isZero E | let 表达式叠放 | 定义的规则 | 条件语句的使用

💬 写在前面:从现在开始,让我们正式设计和实现编程语言。首先,让我们扩展在之前定义的整数表达式语言,以便可以使用变量和条件表达式。 目录 0x00 文法结构 0x01 真假表达式:isZero E 0x02 let 表达式叠…

1-5题查询 - 高频 SQL 50 题基础版

目录 1. 相关知识点2. 例题2.1.可回收且低脂的产品2.2.寻找用户推荐人2.3.大的国家2.4. 文章浏览 I2.5. 无效的推文 1. 相关知识点 sql判断,不包含null,判断不出来distinct是通过查询的结果来去除重复记录ASC升序计算字符长度 CHAR_LENGTH() 或 LENGTH(…

LeetCode-Leetcode 1120:子树的最大平均值

LeetCode-Leetcode 1120:子树的最大平均值 题目描述:解题思路一:递归解题思路二:0解题思路三:0 题目描述: 给你一棵二叉树的根节点 root,找出这棵树的 每一棵 子树的 平均值 中的 最大 值。 子…

文章浮现之单细胞VDJ的柱状图

应各位老师的需求复现一篇文章的中的某个图 具体复现图5的整个思路图,这里没有原始数据,所以我使用虚拟生产的metadata进行画图 不废话直接上代码,先上python的代码的结果图 import matplotlib.pyplot as plt import numpy as np# 数据&#…

RocketMQ常用基本操作

文章中的rabbitmq使用的是rocketmq-all-5.1.3-bin-release版本,需要安装包的可自行下载 RockerMQ启动停止命令 启动命令 nohup sh bin/mqnamesrv & nohup sh bin/mqbroker -n localhost:9876 --enable-proxy & 查看日志 tail -f ~/logs/rocketmqlogs/…

sqlmap注入详解

免责声明:本文仅做分享... 目录 1.介绍 2.特点 3.下载 4.帮助文档 5.常见命令 指定目标 请求 HTTP cookie头 HTTP User-Agent头 HTTP协议的证书认证 HTTP(S)代理 HTTP请求延迟 设定超时时间 设定重试超时 设定随机改变的参数值 利用正则过滤目标网址 避免过多的…

江大白 | 何凯明入职 MIT,首次带队提出Diffusion Loss,扩散模型思想提升生成速度和效果 !

本文来源公众号“江大白”,仅用于学术分享,侵权删,干货满满。 原文链接:何凯明入职 MIT,首次带队提出Diffusion Loss,扩散模型思想提升生成速度和效果 ! 导读 在图像生成领域中,作…

在数字化转型中,中小企业如何打造数字化产品和服务?

引言:随着社会的发展和消费者行为的变化,市场对数字化产品和服务的需求日益增长。中小企业需要紧跟这一趋势,通过开发数字化产品和服务来满足消费者的新需求。云计算、大数据、人工智能等先进技术的出现,为中小企业提供了更多的机…

志愿填报指南:为什么我强烈建议你报考计算机专业

首先恭喜2024届高考的同学们,你们已经通过了高考的考验,即将进入人生的新阶段——大学。 现在正是高考完填报志愿的时刻,Left听到身边朋友提到报考志愿的诸多问题: 志愿填报怎么填?我要报考什么专业?这个…

基于线调频小波变换的非平稳信号分析方法(MATLAB)

信号处理领域学者为了改进小波变换在各时频区间能量聚集性不高的缺点,有学者在小波分析基础上引入调频算子构成了线性调频小波变换,线调频小波一方面继承了小波变换的理论完善性,另一方面用一个新的参数(线调频参数)刻…

ONLYOFFICE 桌面编辑器 8.1 现已发布:功能完善的 PDF 编辑器、幻灯片版式、改进从右至左显示、新的本地化选项等

继 ONLYOFFICE 文档 8.1 发布后,适用于 Linux、Windows 和 macOS 的 ONLYOFFICE 桌面应用程序最新版本也已推出。它具有在线套件的最主要功能,例如功能齐全的 PDF 编辑器、演示文稿中的幻灯片版式、改进的 RTL 支持、新的本地化选项等。 点击进入ONLYOF…

vue中【事件修饰符号】详解

在Vue中,事件修饰符是一种特殊的后缀,用于修改事件触发时的默认行为。以下是Vue中常见的事件修饰符的详细解释: .stop 调用event.stopPropagation(),阻止事件冒泡。当你在嵌套元素中都有相同的事件监听器(如click事件…

100张linux C/C++工程师面试高质量图

文章目录 杂项BIOSlinux开机启动流程内核启动流程网络编程网络编程流程tcp状态机三次握手四次断开reactor模型proactor模型select原理poll原理epoll原理文件系统虚拟文件系统文件系统调用阻塞IO非阻塞IO异步IO同步阻塞同步非阻塞IO多路复用进程管理进程状态程序加载内存管理MMU…

【高中数学/基本不等式】已知:x,y皆为正实数,且2xy+x+6y=6 求:x+2y的最小值

【题目】 已知:x,y皆为正实数,且2xyx6y6 求:x2y的最小值 【解答】 解法一:因为2xyx6y6 可转换为(x3)(2y1)-36 得到(x3)(2y1)9 而x2yx3-32y1-1 (x3)(2y1)-4 >2*根号下[(x3)(2y1)]-4 2*3-4 2 解法二&#xff1a…

5.4符号三角形问题

#include<iostream> #include<stdio.h> using namespace std; int half; int ssum; int cnt0;//减号的个数 int n; int p[100][100]; int countt0; void BackTrack(int s) {if(cnt>half||s*(s-1)/2-cnt>half)return ;if(s>n){countt;return ;}for(int i0;…

【从零开始学架构 架构基础】五 架构设计的复杂度来源:低成本、安全、规模

架构设计的复杂度来源其实就是架构设计要解决的问题&#xff0c;主要有如下几个&#xff1a;高性能、高可用、可扩展、低成本、安全、规模。复杂度的关键&#xff0c;就是新旧技术之间不是完全的替代关系&#xff0c;有交叉&#xff0c;有各自的特点&#xff0c;所以才需要具体…

Android Studio环境搭建(4.03)和报错解决记录

1.本地SDK包导入 安装好IDE以及下好SDK包后&#xff0c;先不要管IDE的引导配置&#xff0c;直接新建一个新工程&#xff0c;进到开发界面。 SDK路径配置&#xff1a;File---->>Other Settings---->>Default Project Structure 拷贝你SDK解压的路径来这&#xff0c;…

Hugging Face发布重量级版本:Transformer 4.42

Hugging Face 宣布发布Transformer 4.42&#xff0c;该版本为流行的机器学习库带来了许多新功能和增强功能。此版本引入了几个高级模型&#xff0c;支持新工具和检索增强生成 &#xff08;RAG&#xff09;&#xff0c;提供 GGUF 微调&#xff0c;并整合了量化的 KV 缓存&#x…

2029年AI服务器出货量将突破450万台,AI推理服务器即将爆发式增长

在2020年&#xff0c;新冠疫情与远程办公模式的兴起推动了所有类型服务器的出货量达到峰值&#xff0c;随后几年里&#xff0c;除了AI服务器之外的所有类别都回归到了正常水平。 根据Omdia的研究数据&#xff0c;AI服务器的出货量在2020年急剧上升&#xff0c;并且至今未显示出…

每日一题——Python实现PAT乙级1073 多选题常见计分法(举一反三+思想解读+逐步优化)9千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 初次尝试 再次尝试 有何不同 版本一&#xff08;原始版本&#xff09;&#xff1a;…