ThreadPoolExecutor线程池核心原理与参数配置:从源码到线上故障排查
入职第三年我第一次在线上碰到线程池被打满的故障。业务高峰期某个核心接口的响应从 200ms 一路飙升到 3 秒还在往上走监控面板上线程池的队列深度一个多小时没降下来。事后复盘根子不在代码逻辑而在大家对 ThreadPoolExecutor 执行顺序的理解不一致——有人以为核心线程满了就该直接新建线程有人以为队列永远能兜底结果参数一配行为完全不是预期。其实 ThreadPoolExecutor 这套东西网上讲原理的文章一抓一大把但大多数人看完还是糊的看着源码能读合上源码就忘。这篇文章我不想从头到尾背一遍官方注释而是把它的实现原理拆成几个核心问题来讲线程池到底在解决什么问题、任务进来之后按什么顺序流转、Worker 线程是怎么诞生的、又是怎么被回收的、线程池状态怎么切换、参数该怎么根据场景去配。每一块我都会结合源码、运行机制和实际踩过的坑一起说适合正在准备面试、刚接手线上系统、或者想自己写一个线程池管理组件的人参考。1. 线程池设计的四个关键问题从一次线上事故说起1.1 为什么需要线程池线程创建开销与池化思想先说个最基础的话题直接new Thread()去执行一个任务到底慢在哪里。线程不是纯粹的用户态对象它背后对应着操作系统的一个内核线程或者轻量级进程。创建一个线程JVM 要分配栈空间默认 1MB 左右操作系统要建立线程控制块、参与调度器的管理。创建和销毁都有开销销毁阶段还涉及内核对象的释放。如果你是一个高并发接口每秒来几千个请求每个请求都走新建线程 → 执行 → 销毁线程这条路CPU 时间就大量浪费在线程本身的创建和销毁上这是第一笔隐性成本。第二笔成本是资源损耗。线程多了光栈空间就要吃不少内存而且线程上下文切换也会抢占 CPU。假设一个任务本身只执行 2ms但创建线程花 1ms、销毁又花 1ms那额外开销占到了整个任务耗时的 50%。线程池的池化思想就是把线程创建的开销前置。启动时先准备好一批线程任务来了直接复用执行完一个任务再去取下一个线程本身不销毁直到超时或线程池关闭。这和数据库连接池、HTTP 连接池是一个逻辑复用比重建便宜得多。1.2 ThreadPoolExecutor 的构造参数七个参数每个都是决策点ThreadPoolExecutor 的完整构造函数有七个参数很多人背参数名都能背但没想过每个参数后面到底代表了一种什么权衡。corePoolSize核心线程数可以理解为池子的保底兵力。maximumPoolSize最大线程数池子能膨胀的上限。keepAliveTime TimeUnit非核心线程空闲多久后被回收。workQueue任务队列核心线程忙不过来的时候任务先放哪里。threadFactory线程工厂决定线程怎么创建、怎么命名。handler拒绝策略池子和队列都满的时候怎么办。从设计角度看这七个参数其实在回答四个问题池子里最少留多少人corePoolSize、最多能扩到多少人maximumPoolSize、人不够的时候任务先放在哪workQueue、放不下的怎么办handler。keepAliveTime 和 threadFactory 都是在补充边界条件。很多线上问题都出在参数之间互相影响上。比如 maximumPoolSize 设得很大但 workQueue 用的是无界队列那么 maximumPoolSize 除了在队列为空的极端瞬间有机会生效其余时间形同虚设。再比如 keepAliveTime 设太短业务稍有波动非核心线程就被回收下个高峰又重新创建线程数量反复抖动。1.3 ctl 字段一个 Int 变量如何装下状态与数量看 ThreadPoolExecutor 源码最先要搞清楚的是一个叫ctl的字段private final AtomicInteger ctl new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS Integer.SIZE - 3; private static final int CAPACITY (1 COUNT_BITS) - 1;这个设计很巧妙。ctl是原子的但只用了一个 int 同时存两样东西高 3 位存线程池运行状态低 29 位存工作线程数量。为什么能这么做因为 29 位足够表示 5.36 亿个线程实际业务里根本不会达到这个量级。private static int runStateOf(int c) { return c ~CAPACITY; } private static int workerCountOf(int c) { return c CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; }workerCountOf用低 29 位拿线程数runStateOf用高 3 位拿状态。用一个原子变量把两个经常要同时判断的数据合在一起就避免了加锁。后面看 execute、addWorker、getTask 的时候会发现大量并发判断都是基于ctl的 CAS 操作这就是它的意义所在。我在看源码时最大的体会是如果不用 ctl 这种位设计线程池的并发控制会复杂得多因为你必须保证读取状态和修改线程数这两个操作在并发下完全一致。现在一个compareAndIncrementWorkerCount就把判断数量没超上限并增加计数合成了单步操作。2. 任务提交后的完整决策链路execute() 源码逐行拆解2.1 四个分支优先开辟还是先入队跟直觉不一样很多人的直觉是线程不够了先开新线程新线程也不行了再扔队列。但 ThreadPoolExecutor 的真实流程不是这样的。来看 execute 方法的核心四段public void execute(Runnable command) { if (command null) throw new NullPointerException(); int c ctl.get(); if (workerCountOf(c) corePoolSize) { if (addWorker(command, true)) return; c ctl.get(); } if (isRunning(c) workQueue.offer(command)) { int recheck ctl.get(); if (! isRunning(recheck) remove(command)) reject(command); else if (workerCountOf(recheck) 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }第一步如果当前工作线程数小于核心线程数直接尝试新建一个核心线程去执行这个任务。注意这里用的是尝试因为 addWorker 内部要 CAS 更新 ctl一旦失败说明并发下别人已经把核心线程建满了流程继续往下走。第二步如果核心线程已经满或者刚才 addWorker 失败就尝试把任务放进队列。放队列之前还加了个isRunning(c)判断——线程池都关闭了就别往里塞任务了。第三步如果队列也放不进去比如队列已满或用了不存任务的队列才会尝试addWorker(command, false)也就是新建非核心线程。这里的非核心线程没有保底空闲超过 keepAliveTime 会被回收。第四步如果连非核心线程都建不了也就是当前线程数已经到了 maximumPoolSize那么只能走拒绝策略。所以真实顺序是核心线程 → 队列 → 非核心线程 → 拒绝。核心线程满了之后优先把任务排队而不是无脑扩线程。这个设计背后的逻辑是线程数是昂贵资源不能一有流量波动就疯狂建线程先用队列把波峰缓存下来尽力用已创建的线程慢慢消化。队列都进不去再扩非核心线程这个顺序让很多人栽过跟头。比如一个接口调用第三方服务单次调用耗时 2 秒核心线程数 10队列大小 100最大线程数 50。并发请求一上来前 10 个请求占了核心线程后面 100 个排队再后面的请求才会触发创建新的非核心线程。如果你以为最大 50 个线程应该能扛更多并发那就错了前 110 个请求都卡在队列里响应时间必然是队列等待 2 秒处理时间。2.2 任务队列的三类选择直接影响非核心线程的触发时机队列类型决定了线程池的行为模式这一点经常被忽略。Java 里常用的有三类队列类型是否存储任务典型场景与影响SynchronousQueue不存储任务直接交给工作线程没有等待环节适合需要低延迟、快速转交的场景LinkedBlockingQueue可无界可有界默认无界时任务可以无限排队maxPoolSize 参数基本失效ArrayBlockingQueue有界背压能力明确队列满之后才会触发非核心线程和拒绝策略用 SynchronousQueue 的时候每个 put 必须等到有 take 才能成功也就是说没有一个线程空闲等着接任务offer 就会失败立刻走 addWorker 创建新线程。这种队列配合一个比较大的 maximumPoolSize可以做到任务来了基本不排队。但代价是线程数会被顶得很高。LinkedBlockingQueue 如果用无界队列任务基本不丢可一旦生产者速度持续高于消费者队列会越长越大内存占用持续上涨而且你设置的 maximumPoolSize 和 keepAliveTime 几乎都变成了摆设因为根本没有触发时机。这也是很多配置了最大线程数但没生效问题的根源。ArrayBlockingQueue 是有界队列有明确的容量上限。它是实际项目里推荐优先考虑的因为可以精确控制任务的等待深度。配合合理的 maximumPoolSize当队列满时还能临时扩一批线程帮扛压力。我在实际项目里通常用有界队列队列大小根据业务的可接受等待时间来估单任务平均处理时间假设 100ms希望一个请求在队列里的最大等待不超过 1 秒那队列大小差不多就是 10 个任务量乘以线程数。当然这只是粗略思路真实的容量需要结合压测数据迭代调整。2.3 四种拒绝策略从抛异常到调用者执行当线程池里的线程已经到达 maximumPoolSize、队列也满了再提交任务就会触发 RejectedExecutionHandler。JDK 提供了四个现成的实现但它们的安全级别完全不同。AbortPolicy默认策略直接抛出 RejectedExecutionException。适合核心业务起码你能通过异常感知到系统确实过载了。CallerRunsPolicy不丢任务把任务退回给调用者线程执行。这个策略在实际项目中经常被低估它天然提供背压调用者线程被占用调用速度自然放慢。DiscardPolicy静默丢弃直接丢弃新任务不让调用者感知。DiscardOldestPolicy丢弃队列里最老的一个任务然后尝试重新提交新任务。从实现上看AbortPolicy 的代码最简单public final class AbortPolicy implements RejectedExecutionHandler { public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException(Task r.toString() rejected from e.toString()); } }CallerRunsPolicy 是这四个里我个人最喜欢推荐的一种。它不会让任务凭空消失也不会打断调用方。代价是调用线程本来要立刻返回现在却要同步执行一个任务这会让调用方本身变慢但对整个系统而言反而是保护——它把压力的源头堵住了。DiscardPolicy 和 DiscardOldestPolicy 要想清楚才能用。前者适合一些可以容忍丢失、由定时任务周期刷新的场景后者适合队列里任务的时效性很强、旧的还没执行完就已经失去意义的场景。需要特别提醒拒绝策略触发一次通常就是过载信号不要只抛异常就完事最好在自定义 handler 里加监控上报把拒绝次数、任务内容、当时线程池的运行指标打出来。日志里看到 threadPool.rejectCount 开始增长时基本是容量规划的警告灯亮了。3. Worker 线程的一生从创建到销毁的全过程3.1 Worker 为什么继承 AQS独占锁与不可重入的含义线程池里的工作线程不是裸的 Thread 对象而是被包装成了一个叫 Worker 的内部类。每一个 Worker 持有一个线程以及一个初始任务firstTask。Worker 继承了 AbstractQueuedSynchronizer也就是 AQS。很多人不理解一个干活的工作线程为什么要搞一个锁出来答案很简单这个锁不是为了保护业务逻辑而是为了标记这个线程是不是正在干活。线程池在 shutdown 或调整参数时有时需要中断某些空闲线程。如果一个线程正在执行任务中途被打断很可能导致数据不一致而如果线程空闲在队列上等待中断它让它退出就相对安全。所以 Worker 用自己的 AQS 实现了一个不可重入的独占锁正常执行任务前会调用lock()任务结束在 finally 里unlock()。线程池想中断空闲线程时先尝试tryLock()拿不到锁说明线程正在干活就不打断它。这里有一个设计细节为什么不直接用标准的 ReentrantLock因为 AQS 语义里如果一个线程持锁后再次请求同一把锁ReentrantLock 会成功重入拿不到正确的忙碌信号。而 Worker 的自定义 AQS 实现中整个 runWorker 流程只允许锁被持有一次多余的尝试直接返回失败。这样就天然区分了正在执行任务和空闲等待任务两种情况。Worker 构造方法里还有一句Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask firstTask; this.thread getThreadFactory().newThread(this); }初始化时 state 设为 -1是为了防止线程在真正开始 runWorker 之前被人中断。如果 Worker 正在构造、线程还没启动这时候有别的线程调了 shutdownNow 并抢着中断所有工作线程这个 -1 状态能让中断行为不生效。等 runWorker 里真正要跑任务时再调用unlock()会把 state 从 -1 恢复为 0。这种细节不读源码很难发现。3.2 addWorker 的双重检查状态与数量的并发边界addWorker 是线程池里最核心的方法之一它的任务是按参数要求新建一个工作线程但并发环境下必须做两件事检查线程池状态是否允许新建、检查线程数量是否超限。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); if (wc CAPACITY || wc (core ? corePoolSize : maximumPoolSize)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c ctl.get(); if (runStateOf(c) ! rs) continue retry; } } ... }第一次检查针对状态线程池已经 SHUTDOWN 以上时正常情况下不允许再增加新线程。唯一的例外是 SHUTDOWN 状态且队列里还有任务此时可以额外创建线程来消化队列剩余任务这就是firstTask null !workQueue.isEmpty()的含义。第二次检查针对数量核心线程看 corePoolSize非核心线程看 maximumPoolSize如果已经达到上限就直接拒绝创建。CAS 成功后跳出循环接下来才真正构造 Worker 对象、加入 workers 集合、启动线程。外层 for 循环里的continue retry是我认为最精彩的部分。CAS 失败后重新读取 ctl如果线程池状态已经变了就从外层的状态检查重新开始否则只重试内层数量判断。这种双层循环的设计保证了状态校验和数量校验在并发场景下不出现边检查边变化的窗口。实际操作中addWorker 失败通常意味着两种情况要么是线程数真的到了上限要么是线程池状态已经不再接受新线程。看到 addWorker 返回 false紧接着就会进入拒绝流程这也是很多任务莫名其妙被丢弃的原因。3.3 核心线程会死吗getTask 与 keepAliveTime 的真相第一个要纠正的认知是核心线程并不保证永远存活。默认情况下确实不会因为超时被回收但它同样会从队列里取不到任务时进入阻塞等待。而一旦设置了 allowCoreThreadTimeOut(true)核心线程空闲超过 keepAliveTime 后也会退出。Worker 启动后的执行逻辑在 runWorker 里核心是一个 while 循环反复调用 getTask 取任务try { while (task ! null || (task getTask()) ! null) { w.lock(); ... try { task.run(); } finally { w.unlock(); } ... } } finally { processWorkerExit(w, completedAbruptly); }getTask 是理解 keepAliveTime 的关键Runnable r timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();当线程数大于 corePoolSize或者允许核心线程超时回收时timed 为 true工作线程会从队列里 poll 一个任务如果 keepAliveTime 内没有任务进来poll 返回 nullgetTask 返回 null循环退出线程自然结束。很多刚接触线程池的人以为 keepAliveTime 是线程空闲多久就被杀掉严格说应该是空闲线程在取不到任务时等待了多久就自杀。它和队列类型有很强的联动。如果你用 SynchronousQueue那么只要核心线程都忙后面每个任务都会创建新线程当任务处理完了多余的非核心线程在 keepAliveTime 时间一到就会回收。如果任务持续不断线程数会一直顶到 maximumPoolSize直到流量降下来才慢慢回收。这里有个隐藏问题如果核心线程数设得很大而任务又不多队列使用 take() 会导致这些核心线程全部阻塞在队列上。虽然这不消耗太多 CPU但每个线程占着栈内存数量一旦上万依然很可观。所以我一直建议不要盲目设置大核心线程数宁可队列稍大、非核心线程做缓冲也不要一上来就预留几百个线程睡觉。还有一个非常经典的陷阱任务队列用无界队列时getTask 里的 poll 超时回收几乎不会触发因为线程数永远不会超过 corePoolSizetimed 永远是 false所有线程都在 take() 上无限等待。大白话讲就是 keepAliveTime 根本没机会生效。4. 线程池状态机与优雅关闭流程4.1 五种线程池状态RUNNING 到 TERMINATED 的迁移规则ThreadPoolExecutor 定义了五种线程池运行状态。它们不是随意定的名字而是对应了不同的行为边界。状态含义接受新任务处理队列任务中断工作线程RUNNING正常运行是是否SHUTDOWN关闭中否是否STOP紧急停止否否是TIDYING收尾中否否否TERMINATED已终止否否否状态迁移的路径是单向的只能往后走不能回退。RUNNING → SHUTDOWN 靠 shutdown()RUNNING 或 SHUTDOWN → STOP 靠 shutdownNow()SHUTDOWN 且队列为空、或者 STOP 后线程全退出就进入 TIDYING最后执行 terminated() 钩子方法进入 TERMINATED。为什么要有这么多种状态而不是一个布尔标记因为关闭线程池不是瞬时行为正在执行的任务需要给一个缓冲周期队列里堆积的任务也可能需要被消化完。状态机存在的意义就是让关闭这件事变成分阶段、可控的过程。从 ctl 的高 3 位设计也能看出来状态和线程数是绑定在一起的比如线程池进入 TIDYING 的前提是 workerCount 已经变成 0这两件事必须作为一个整体原子地观察和修改。4.2 shutdown() 与 shutdownNow() 的差别谁还在等谁被打断很多人把 shutdown 和 shutdownNow 混着用觉得都是关线程池但实际行为差异非常大。shutdown() 会把状态改为 SHUTDOWN不再接受新任务但队列里已有的任务会继续执行完。它是温柔关闭能保证已经提交的任务不丢。如果线程池还有线程在 take() 上等待shutdown 会让它们继续把队列取空取空之后才陆续退出。shutdownNow() 则把状态改为 STOP它会中断所有工作线程并返回当前队列里尚未执行的任务列表。只调用方需要自己决定这些未执行任务怎么办。注意这里的中断是调用了 interrupt()但如果任务里没有对中断响应比如正在执行一个不处理 InterruptedException 的循环线程实际还是会继续跑下去。这也是很多人误解的地方——shutdownNow 不是强杀线程只是发送了中断信号接不接收还得看任务代码。从源码上看shutdownNow 里有个interruptWorkers()它会对所有 worker 发起中断。但注意这个中断也会把正在执行任务的线程打断所以如果任务对中断敏感可能执行一半就退出。再看 Worker 里的 tryLock 机制在 shutdown 场景里中断的是空闲线程shutdownNow 场景里不再区分空闲与否所有 worker 都会收到中断。从使用经验来看shutdownNow 返回的未执行任务列表一定要正确处理。常见做法是拿到这些 Runnable 之后转交给另一个线程池或者持久化到消息队列里等待后续补偿。否则下次数据对账就会发现一批任务凭空消失。4.3 优雅关闭的正确姿势与常见误区优雅关闭线程池在线上通常要分成三步第一步调用 shutdown()禁止新任务提交让队列里的存量任务继续消化。第二步调用 awaitTermination(timeout, unit)阻塞等待线程池真正终止。第三步如果超时还没终止再调用 shutdownNow()强制中断残余任务并对返回的未完成任务做补偿处理。这个流程的本质是先给存量任务一个自然死亡的窗口只有在这个窗口内没完全结束才启动强制的后续手段。网上说的先 shutdown 再 shutdownNow就是这个套路。executor.shutdown(); if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { executor.shutdownNow(); ListRunnable remaining executor.shutdownNow(); // 补偿处理剩余任务 }上面这段有个细节需要注意如果第一次 awaitTermination 超时说明可能已经有任务卡住了我一般会再调一次 shutdownNow然后对 remaining 做持久化补偿。但这段代码里两次调用都在同一段流程中可能看起来重复实际应用中我会把第二次 shutdownNow 的结果妥善落库或者转发到备份队列。还有一个高频误区线程池的线程设置成非守护线程Spring 容器关闭时如果没有主动关闭线程池整个进程会一直卡着无法退出。很多应用部署时的停机时间过长排查下来经常是某个线程池没有放在 destroy 逻辑里。5. 参数配置、避坑清单与实际监控经验5.1 线程数到底怎么定从 CPU 密集到 IO 密集的估算思路配置线程池参数最难的是回答一个问题corePoolSize 和 maximumPoolSize 到底设多少。对 CPU 密集型任务线程数一般建议是 CPU 核数 N 或者 N1。加 1 的原因是让某个线程偶尔发生页缺失、IO 等待时其他线程能补位。这个公式背后的原理很简单CPU 密集型任务的耗时几乎都在计算线程数超过核心数之后多出来的线程只是在抢时间片刷新了线程切换时间占比这个指标吞吐量反而可能下降。对 IO 密集型或阻塞型任务真正经典的公式是线程数 CPU 核数 × (1 平均等待时间 / 平均计算时间)。这里等待时间包括远程调用耗时、数据库等待、锁等待等。举个例子四核机器任务平均执行 10ms其中 2ms 在 CPU 计算8ms 在等下游响应。那么线程数 4 × (1 8/2) 20。为什么这么算因为这个任务在 10ms 的执行周期里只有 2ms 在占用 CPU其余 8ms 线程闲着。想要把四核的 CPU 用满需要 4 / (2/10) 20 个线程才能让任务重叠起来。这个公式是我平时做容量评估的起点然后通过压测往上调整。队列大小我习惯配合可容忍的排队时延来定。假设每个任务平均处理时间是 10ms希望队列等待不超过 2 秒那队列里最多允许存 200 个任务。当然实际业务中任务处理时间会波动所以我会把估算值再乘上一个安全系数同时监控队列深度变化来不断校正。5.2 一份常见问题与排查速查表现象可能原因排查思路线程数一直卡在 corePoolSizemaxPoolSize 没生效使用了无界队列任务永远不会触发非核心线程检查 workQueue 类型改用有界队列任务频繁被拒绝队列太小或 maximumPoolSize 太低看拒绝次数与队列深度调整参数或做容量扩容线程反复创建销毁keepAliveTime 太短、流量波动大调大 keepAliveTime或设置 allowCoreThreadTimeOut(false)应用停机很慢线程池没有优雅关闭在容器销毁逻辑里调用 shutdown awaitTermination内存持续上涨无界队列堆积任务改用有界队列并监控队列积压线程池任务执行异常却看不出原因没有自定义 RejectedExecutionHandler加日志、加监控至少记录任务信息和池状态有一个排查经验值得单独提出来线上如果出现线程数一直不增长、但队列在膨胀的情况不要急着加 maximumPoolSize要先去看队列类型。无界队列场景下加 maximumPoolSize 是无效操作因为任务根本没有机会走到创建非核心线程的分支。5.3 线上监控的三个轻量方案与阅读源码的建议线程池监控我不太建议一上来就引入很重的监控平台先做轻量级的活体监测就够了。最简单的方案是开一个定时任务定期打印线程池的几个关键指标活跃线程数、队列大小、已完成任务数、拒绝次数。我用过一种比较顺手的方式在封装线程池管理组件时把 ThreadPoolExecutor 包一层定时输出它的明细到日志同时提供一个指标接口供监控系统拉取。核心指标有四个getActiveCount()、getQueue().size()、getCompletedTaskCount()、getTaskCount()。这四个指标组合起来能看出很多事情。比如活跃线程数接近 corePoolSize、队列在持续增长说明处理能力跟不上了完成数不增长说明任务已经卡住。再进一步可以监控线程池生命周期中的关键事件。用 ThreadPoolExecutor 自带的钩子方法 beforeExecute、afterExecute 可以记录单任务执行时间用来发现长尾任务。这两个钩子方法在源码里默认是空实现留给子类覆盖。很多真实的性能问题就是从某个任务执行时间特别长开始的。关于读源码我建议先读 execute、getTask、runWorker、addWorker 这四个方法的完整代码再去看 Worker 的内部实现。不要按文件一行一行背而是带着问题去读任务怎么进来、线程怎么出来、线程怎么处理队列、线程怎么退出。把这四个问题串起来线程池的骨架就清楚了。之后再补充看 shutdown 和 tryTerminate 之类的状态流转方法整个知识面就完整了。按我个人经验线程池的问题很少是单个参数导致的更多是参数之间配置互相打架。看代码时关注 ctl 的高三位和低 29 位如何被各种方法读取和修改就很容易理解为什么这里要 CAS、那里要双重检查了。真正用过一段时间、在线上排过几次故障之后这些设计就不再是死记硬背的知识点而变成了顺理成章的东西。

相关新闻

Apache SkyWalking实战:无侵入分布式链路追踪与排坑指南

Apache SkyWalking实战:无侵入分布式链路追踪与排坑指南

讲真,服务一拆成微服务,线上问题排查那叫一个酸爽。以前单体应用出问题,翻翻日志、看看堆栈基本就能定位;现在一个请求从网关进来,经过四五个服务,调了三四个数据库和缓存,到底卡在哪儿、失败出…

2026/10/9 3:58:28 阅读更多 →
华为eNSP跨交换机VLAN实验:Access与Trunk配置及802.1Q抓包

华为eNSP跨交换机VLAN实验:Access与Trunk配置及802.1Q抓包

简介:这份资源是面向计算机网络专业学生、网络管理员及网络技术初学者的跨交换机VLAN配置实验文档,基于华为eNSP模拟软件,帮助读者掌握复杂交换式以太网的设计流程与VLAN划分方法。内容围绕跨交换机VLAN划分、接入端口与主干端口的功能差异、…

2026/10/9 3:58:28 阅读更多 →
华科计院网络实验报告:从VLAN划分到SOCKET编程的完整链路

华科计院网络实验报告:从VLAN划分到SOCKET编程的完整链路

简介:这份华中科技大学计算机学院《计算机网络》实验报告,面向正在学习网络课程、需要完成组网与路由配置实验的高校学生及自学者。内容围绕IP协议、网络层与数据链路层协议、子网划分、RIP与OSPF路由协议、路由器及二/三层交换机配置、VLAN划分和访问控…

2026/10/9 3:58:28 阅读更多 →

最新新闻

LRE框架:重构AI智能体的时间感知与因果记忆机制

LRE框架:重构AI智能体的时间感知与因果记忆机制

1. 这不是“给AI加个备忘录”,而是重构智能体的时间感知能力很多人第一次看到“AI智能体记忆管理”这个词,下意识会想:不就是让大模型多存点上下文、加个向量数据库当外挂硬盘吗?我试过——在某个模拟项目X里,给一个任…

2026/10/9 4:48:03 阅读更多 →
10分钟上手 douyin-downloader:抖音批量下载与无水印提取完整指南

10分钟上手 douyin-downloader:抖音批量下载与无水印提取完整指南

10分钟上手 douyin-downloader:抖音批量下载与无水印提取完整指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fal…

2026/10/9 4:48:03 阅读更多 →
answer-me-with-html 完整参考指南:从 Markdown 稿件格式到代码块、自定义主题与 STE 写作检查

answer-me-with-html 完整参考指南:从 Markdown 稿件格式到代码块、自定义主题与 STE 写作检查

【免费下载链接】answer-me-with-html Answer me with HTML — an agent skill that answers hard questions with a one-page HTML you can actually read. 让 AI Agent 用一页 HTML 回答复杂问题。 项目地址: https://gitcode.com/gh_mirrors/an/answer-me-with-h…

2026/10/9 4:48:03 阅读更多 →
ponytail插件使用指南:skill模块配置与效率提升实践

ponytail插件使用指南:skill模块配置与效率提升实践

1. 从“ponytail”这个标题说起:它到底是什么第一次看到“ponytail”这个词,大多数人脑子里蹦出来的画面是扎在脑后的那束马尾辫。但如果它出现在技术社区、插件市场或者效率工具的讨论里,那它大概率不是发型教程,而是一个被开发者…

2026/10/9 4:48:03 阅读更多 →
Java电商系统实战:JSP+JavaBean+SQL Server全链路可运行方案

Java电商系统实战:JSP+JavaBean+SQL Server全链路可运行方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 4:48:03 阅读更多 →
HermesWorkspace Playground 可选 3D NPC 模型替换指南:基于 GLB 的 Voxel 身体升级方案

HermesWorkspace Playground 可选 3D NPC 模型替换指南:基于 GLB 的 Voxel 身体升级方案

【免费下载链接】hermes-workspace Native web workspace for Hermes Agent — chat, terminal, memory, skills, inspector. 项目地址: https://gitcode.com/gh_mirrors/he/hermes-workspace 点击查看 免费下载 本文依据仓库 public/avatars-3d/README.md 编写&am…

2026/10/9 4:47:02 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →