三线程循环打印ABC:volatile自旋与Semaphore实现对比
先交代一句标题里这个“开启3个线程循环打印10遍ABC”是并发入门里最经典的一道老题三个线程分别负责打印A、B、C每个线程各打印10次最终输出必须是严格重复的“ABCABCABC...”也就是总长度30个字符一个顺序都不能乱。题目看着简单但它把多线程里最容易被忽视的几件事全考了一遍可见性、协作、等待和唤醒。今天我想把两个风格的实现摊开来讲——一个是我理解的“何氏硬刚版”不用任何现成同步工具拿volatile硬扛另一个是网上流传最广的“网红版”用信号量做精确交接。这两条路线我都实际跑过踩过的坑也会一起拿出来。适合看这篇的人很明确准备Java后端面试的、刚接触多线程想找个题目练手的、以及那种“明明写对了但总觉得哪里不对”的同学。读完你至少能说出两套不同方案的代码、它们各自的原理、以及为什么有的写法能在面试里加分、有的写法只能留在笔记本里自嗨。1. 经典题目到底在考什么1.1 三线程接力赛题目本质把这道题理解成一场接力赛最顺手。跑道只有一条三个队员A、B、C轮流跑A跑完把接力棒交给BB跑完交给CC跑完再还给A。这里最关键的一点是任何一个时刻只能有一个队员在跑。如果三个人同时冲出去输出就会变成“ABBAC...”这种鬼样子。很多初学者第一次尝试是直接开三个线程每个线程里面循环打印自己的字母结果发现顺序完全不可控。原因也很简单线程调度是抢占式的A线程打印完一个“A”之后时间片可能刚好用完CPU切到B线程B打印一个“B”再切回来A又打印一个“A”结果就出现了“ABA”。所以这道题的本质不是“如何创建线程”而是“如何让多个线程协作保证同一个时刻只有一个线程在执行打印动作”。这个“同一时刻只有一个线程能打印”的约束就是临界区保护。实现方式可以千奇百怪但核心永远是那个接力棒令牌Token。谁拿到令牌谁打印打印完主动把令牌交给下一个人。1.2 并发考点的四张牌可见性、原子性、有序性、协作我刷这道题最大的感受是它能一口气把并发编程的四个基础考点全串起来。第一张牌是可见性。B线程在自旋里等待状态变成1凭什么能第一时间看到A线程把状态改成1如果没有volatileJVM和CPU可能让B线程一直读到自己线程栈里的旧值于是B永远等不到程序就卡死在循环里。volatile的作用就是保证一个线程的写对其他线程的读是可见的。第二张牌是原子性。state这种操作表面上是一行代码底层其实是“读、改、写”三步。在多线程环境下如果两个线程同时执行state可能互相覆盖。但有意思的是这道题的硬刚版里state并不会出错原因是状态机天然保证了“同一个时刻只有满足条件的那个线程会去改state”所以竞争窗口根本不存在。这个细节很多人没想明白后面我会专门展开。第三张牌是有序性。CPU和编译器为了性能会做指令重排锁和volatile都带内存屏障能在关键位置挡住重排。自旋版里如果state没有volatile不光可见性出问题重排也可能破坏“先打印后改状态”的顺序。第四张牌是协作。线程之间怎么等待、怎么唤醒、谁来唤醒谁这就是Semaphore、Condition、wait/notify这些工具存在的意义。没有协作机制线程就是三个各说各话的广播喇叭。1.3 两条路线的分水岭等待方式不同硬刚版和网红版最大的分水岭不是代码长短而是等待方式。硬刚版的等待方式是自旋也就是不睡觉、不挂起在循环里持续检查状态。它一直在消耗CPU时间片但换来的是极低的延迟——只要令牌一到位当前线程可能正好在跑立刻就能接上。网红版的等待方式是阻塞拿不到信号量许可时线程会真正挂起让出CPU等别人release之后再由操作系统唤醒。这样CPU利用率低但每次唤醒都有一次线程切换的开销。理解了这个分水岭你就能看懂为什么不同方案在不同场景下性能表现差异很大也能明白面试官追问“你觉得哪种好”的时候他想听的不是标准答案而是你对这两种等待方式的理解。2. 何氏硬刚版不用锁也能转起来2.1 核心思路一个状态机转到底“何氏硬刚版”这个名字我理解成一种风格代号。谁写的已经不重要了重要的是这种思路确实够“硬刚”不借助synchronized、不用Lock、不用Semaphore只用一个volatile的int变量硬生生地把三个线程的顺序憋出来。思路是这样的定义一个state0表示轮到A1表示轮到B2表示轮到C。A线程不断检查 state % 3 0成立就打印A然后让state加1B线程检查 state % 3 1成立就打印BC线程同理。当state从2加到3之后state % 3 又变回0A线程接着打印形成一个闭环。这里有一个很容易被忽略的设计顺序判断条件、打印、推进状态这三步的顺序不能乱。必须先打印、再推进状态。为什么因为如果先推进状态再打印A把state从0改成1之后还没来得及输出“A”线程就被切走了B看到state等于1打印了“B”然后A恢复过来又打印“A”顺序就变成了“BA”。反过来先打印再推进A即使打印完“A”就被切走state仍然是0B检查0 % 3 1不成立只能继续等直到A恢复执行stateB才有机会。这就是“先检查、再打印、最后交棒”的意义。2.2 完整代码硬刚版初版直接看代码我加了完整的注释跑的时候用JDK9以上因为用到了Thread.onSpinWait()。public class HardCoreABC { // 0 表示轮到A1 表示轮到B2 表示轮到C // volatile 保证state的修改对其他线程立即可见 private static volatile int state 0; private static final int PRINT_COUNT 10; public static void main(String[] args) { new Thread(() - runTask(A, 0), A).start(); new Thread(() - runTask(B, 1), B).start(); new Thread(() - runTask(C, 2), C).start(); } private static void runTask(char ch, int id) { for (int i 0; i PRINT_COUNT; i) { // 自旋等待一直等到 state % 3 id while (state % 3 ! id) { // 告诉CPU这里是一个自旋等待循环可以优化功耗 Thread.onSpinWait(); } System.out.print(ch); state; } } }跑完之后控制台应该打印出一串“ABCABCABCABCABCABCABCABCABCABC”正好30个字符。这个版本里Thread.onSpinWait()很关键。如果去掉它程序也能跑但CPU会以最高频率空转风扇呼呼转。它实际上是在给处理器发一个PAUSE指令告诉CPU“我在等一个东西你不要把流水线塞满”能明显降低功耗和总线竞争。JDK8及以下没有这个方法可以退而求其次用Thread.yield()但效果没有onSpinWait好。2.3 硬刚版的三个致命伤硬刚版看起来很酷但它的问题也非常明显我在实际跑的时候一一踩过。致命伤一CPU占用极高。三个线程同时运行其中两个基本都在空转等状态。自旋不会让出CPU所以它们会持续占用两个逻辑核心如果线程被分配在不同核上。我在一台8核笔记本上跑任务管理器里直接看到两个线程把CPU拉满。题目只打印30个字符刚够让风扇转一下如果循环次数改成10万电脑基本没法干别的。致命伤二依赖volatile的可见性写错一个修饰符就死循环。我第一次写这个版本的时候忘了在state前面加volatile结果A打印了一个A之后B线程一直读不到state更新程序就卡死在B的自旋循环里。这个问题在本地跑的时候不是每次必现因为有些环境恰好能读到新值但一旦遇到就是“偶现卡死”排查起来非常痛苦。致命伤三严格来说它并不是真正的无锁。System.out.print内部是synchronized方法PrintStream写字符的时候会加锁。也就是说线程在打印字符的那一刻实际上依然处于一个锁竞争里。这就让“硬刚版”的名号变得有点虚——三线程的协作确实没用锁可最终的字符输出还是走了锁。2.4 硬刚版的补救CAS自旋版如果你非要较真“无锁”这件事可以换一种方式推进状态用AtomicInteger CAS把“读state、判断、改state”这一串操作里的问题解决得更干净。import java.util.concurrent.atomic.AtomicInteger; public class CasABC { private static final AtomicInteger state new AtomicInteger(0); private static final int PRINT_COUNT 10; public static void main(String[] args) { new Thread(() - runTask(A, 0), A).start(); new Thread(() - runTask(B, 1), B).start(); new Thread(() - runTask(C, 2), C).start(); } private static void runTask(char ch, int id) { for (int i 0; i PRINT_COUNT; i) { while (true) { int cur state.get(); if (cur % 3 ! id) { Thread.onSpinWait(); continue; } // 状态唯一属于当前线程此时打印不会被打乱 System.out.print(ch); // 打印之后再推进状态交给下一个线程 if (state.compareAndSet(cur, cur 1)) { break; } // 理论上不会走到这里 } } } }注意这段代码里CAS放在打印之后。这不是写错了而是故意保持“先打印、再交棒”的顺序。为什么CAS在这里不会失败因为当前状态state % 3的值只对应一个线程其他线程根本进不了打印分支所以不会有人去并发推进state。加CAS的价值是让state从“普通int加法”变成“原子性推进”以后如果逻辑扩展成多令牌场景这个版本能扛更复杂的条件。但说句实在话这种版本属于“面试加分项”大于“工程实用项”。真到生产环境我不会用这种自旋方案去控制业务线程的顺序成本太高。3. 网红版信号量精确交接为什么最讨喜3.1 为什么Semaphore适合接力赛网上流传最广的版本基本上是Semaphore版。这个方案之所以“网红”是因为它把“令牌”这个抽象直接变成了代码信号量就是一个令牌池acquire是拿令牌release是交令牌。放到这道题里结构非常自然给A线程一个信号量初始许可为1给B和C各一个信号量初始许可为0。只有A线程能拿到许可先打印打印完之后release B的信号量B线程就从阻塞中被唤醒拿到许可打印。然后再release CC打印C再release A完成闭环。这个方案比硬刚版舒服在哪最直观的一点是拿不到令牌的线程会真正阻塞而不是空转。CPU利用率低代码可读性也高基本是字面意思每个线程都在等自己的许可拿到就干活干完把许可交给下一个。面试官看一眼就能明白你的意图。3.2 完整代码Semaphore交接版import java.util.concurrent.Semaphore; public class SemaphoreABC { private static final int PRINT_COUNT 10; public static void main(String[] args) { Semaphore semA new Semaphore(1); Semaphore semB new Semaphore(0); Semaphore semC new Semaphore(0); new Thread(() - runTask(A, semA, semB), A).start(); new Thread(() - runTask(B, semB, semC), B).start(); new Thread(() - runTask(C, semC, semA), C).start(); } private static void runTask(char ch, Semaphore current, Semaphore next) { for (int i 0; i PRINT_COUNT; i) { try { current.acquire(); System.out.print(ch); next.release(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } } }这个版本的输出结果和硬刚版完全一样但代码里没有volatile、没有自旋、没有状态机。你只需要记得三个线程各自持有一前一后两个信号量前一个负责给自己发许可后一个负责唤醒下一个线程。InterruptedException的处理不能敷衍。虽然这个Demo里没人会去中断线程但生产环境里线程池关闭、超时控制都可能触发中断。最标准的做法是catch之后重新设置中断标记位然后结束任务不能直接吞掉异常。提示Semaphore默认是非公平模式。在这道题里其实感知不到公平问题因为每个信号量只有一个许可而且只有一个线程在acquire它。但如果在别的场景里多个线程抢同一个信号量非公平模式的“插队”行为就会被放大需要new Semaphore(n, true)开启公平模式。3.3 网红版进阶ReentrantLock Condition比Semaphore更细一点的控制是ReentrantLock加三个Condition。每个线程有自己的等待队列A唤醒BB唤醒CC唤醒A精确到人不会误伤。import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ConditionABC { private static final ReentrantLock lock new ReentrantLock(); private static final Condition condA lock.newCondition(); private static final Condition condB lock.newCondition(); private static final Condition condC lock.newCondition(); private static int state 0; private static final int PRINT_COUNT 10; public static void main(String[] args) { new Thread(() - runTask(A, 0, condA, condB), A).start(); new Thread(() - runTask(B, 1, condB, condC), B).start(); new Thread(() - runTask(C, 2, condC, condA), C).start(); } private static void runTask(char ch, int id, Condition current, Condition next) { for (int i 0; i PRINT_COUNT; i) { lock.lock(); try { // 必须用while不能用if while (state % 3 ! id) { current.await(); } System.out.print(ch); state; next.signal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } } }这段代码的核心是while (state % 3 ! id)这一行。await被唤醒之后线程不会自动判断“我该不该继续”它只会回到await之后的下一行继续执行。如果这里用if判断线程一旦被非正常唤醒虚假唤醒就可能在自己不该打印的时候继续往下走导致顺序错乱。用while重新检查条件是Condition和wait的标准写法没有商量的余地。3.4 为什么不推荐synchronized notifyAll如果你在网上搜这道题还会看到很多用synchronized加wait/notifyAll的版本。思路是多个线程锁同一个对象每次打印完notifyAll唤醒所有线程让它们各自检查状态。功能上它能跑通但我不推荐用这个版本去面试或者写进代码。原因是它有一个很明显的性能缺陷notifyAll会把所有等待线程全部唤醒可实际条件只满足一个其他线程被唤醒后发现不满足条件又乖乖回去睡觉。每次打印都要经历一次“全体起床、检查、再睡回去”的过程在线程多的时候会有明显的惊群效应。如果用notify而不是notifyAll又会遇到另一个问题Java的对象锁只有一个等待集合你没办法指定“只唤醒B线程”。万一唤醒的是CC检查条件不满足就继续wait而B永远不被唤醒程序就死锁了。这个两难困境本质上是因为synchronized的等待机制太粗糙它不区分等待的原因。ReentrantLock的多个Condition就是专门来解决这个问题的——每个Condition一个队列signal只唤醒对应队列的第一个线程。4. 硬刚版与网红版的正面对比4.1 可读性、代码量与门槛单看代码行数硬刚版和Semaphore版差不多都不长。但代码行数少不等于容易懂。硬刚版的理解门槛主要在那个抽象的状态机上你得先把state从0到2的循环想清楚还得理解“先打印后推进状态”的必要性还得意识到volatile承担了什么责任。如果哪一环没想透代码写出来就是“看起来能跑但不知道什么时候会翻车”的状态。Semaphore版则几乎是“所见即所得”线程acquire自己的许可不行就等拿到了就干活然后release给下一个。这种直白感是它成为网红版本的重要原因之一。我给初学者讲这道题也从Semaphore版开始讲讲明白之后再问一句“如果不允许用Semaphore你还能怎么写”再引出硬刚版的状态机。Condition版的可读性居中但它的价值在于让你理解精确唤醒和惊群效应的区别。三套方案如果都能写出来你对线程协作的认知就不是“背了一段代码”而是真的理解了不同抽象层次的控制手段。4.2 性能和资源占用实测对比我实际测试的时候发现一个很尴尬的事实在Java里用System.out.print做控制台输出瓶颈根本不在线程同步而在I/O。30个字符的输出量太小两种方案跑起来时间都差不多肉眼完全看不出差别。想测同步开销就得把打印换成纯内存操作。比如每个线程打印完之后做一个普通的整数自增最后校验总数。我在一台Linux服务器上大致跑过100万轮规律是自旋版总耗时比Semaphore版少一些但CPU占用率明显高有一个核基本被吃满Semaphore版的等待线程会挂起CPU很安静但每次acquire/release都伴随一次线程唤醒整体耗时多了大概两到三成。注意这个结果只能看趋势。自旋在临界区极短、竞争极低的时候确实有性能优势但它把“等待成本”转移到了CPU上。Semaphore把等待成本转给了操作系统的线程调度。没有哪个方案绝对更好只有合不合适。4.3 选型速查表场景推荐方案理由面试讲思路Semaphore版清晰、好写、不容易出错追求底层原理解释volatile自旋版能讲清楚可见性和状态机复杂业务精确唤醒ReentrantLock Condition多条件队列signal精确超短临界区、性能敏感自旋 CAS减少上下文切换但注意CPU占用长等待、大量线程阻塞Semaphore / Lock让出CPU避免空转简单场景、快速实现synchronized代码少但不要用notifyAll5. 现场翻车实录这些问题我都踩过5.1 输出顺序乱套、一直打印同一个字母我第一次写硬刚版跑出来的结果是“AAAAAAAAAA...”B和C全程沉默。原因非常蠢我给state加的不是volatile而是普通int。A线程打印完之后修改stateB线程在另一个核心上读到的state永远是0所以B以为自己永远没轮到自己。加上volatile之后问题立刻消失。还有一种情况是条件写反了比如B线程写成state % 3 ! 0结果B抢在A前面打印输出顺序全乱。排查这类问题最笨也最有效的办法是在每个线程打印之后把当前线程名、字符、state值都打出来看一眼顺序对不对一目了然。注意验证并发程序的正确性别只盯着控制台看。把输出重定向到文件然后用diff和预期的“ABC”重复串比对干净利落。5.2 自旋把CPU飙到100%这是硬刚版最容易劝退人的地方。程序明明跑得很快结果正确但任务管理器里CPU已经红了。原因就是自旋等待时线程不休息持续执行while循环里的状态检查。解决思路有两个方向。一个是在循环体里让出一下Thread.onSpinWait()或Thread.yield()能明显缓解。另一个更彻底——直接放弃自旋换Semaphore或Condition。如果只是写Demo玩加onSpinWait就够了如果代码要部署到生产环境我建议选阻塞方案别拿业务线程的CPU空转开玩笑。5.3 虚假唤醒导致顺序断裂Condition版的经典翻车现场是用if替代while检查条件。某个线程被signal之后恰好不是在正常状态下醒来而是因为某些底层原因被唤醒它直接往下执行打印结果输出顺序就乱了。虚假唤醒在Java文档里专门有说明线程可能在没有被signal、没有被中断、没有超时的情况下醒来。所以await之后必须重新检查条件。这不是理论问题是实际会发生的问题。我写这段代码时吃过亏后来养成了习惯凡是await/wait后面跟条件判断一律用while不用if。5.4 System.out这个隐藏的锁硬刚版标题写的是“不用锁”但实际上System.out.print本身就是synchronized的。也就是说哪怕你的线程协作机制完全无锁字符真正输出到终端的那一步还是要进锁。这个事实有两层影响。第一严格抠“无锁”这个概念时硬刚版站不住脚第二做性能测试时System.out会掩盖真实的同步开销导致自旋版和Semaphore版测出来的时间几乎一样。想测真实差异就得把打印换成纯内存操作比如AtomicLong计数。5.5 常见问题速查表症状可能原因解决办法只打印一个字母然后卡死volatile漏写状态变量加volatile输出顺序错乱条件判断方向写反检查state % 3和id的对应关系CPU占用100%自旋空转不让步加Thread.onSpinWait或换阻塞方案偶发多打印了同一个字母用if而不是while等待改成while循环重新检查条件进程不退出信号量release错了对象检查next是否指向正确下一个线程性能测试看不出区别System.out是瓶颈换成内存计数再测6. 从这道题延伸到工程与面试的思考6.1 面试官问这道题到底想听什么这道题在面试里出现率极高但面试官通常不是只要一个能跑的代码。他更想看到的是你脑子里有没有一套“多线程协作工具箱”。能写出Semaphore版说明你至少会用工具能接着说出Condition版和wait/notifyAll的差异说明你理解等待队列和精确唤醒能主动提起自旋和阻塞的区别、CPU占用和上下文切换的取舍那就是在秀内功了。我遇到过不少候选人代码写得很顺但问到“为什么这里用while不用if”就卡住了——这其实就是考察你有没有真正理解await之后的执行路径。另一个加分点是主动聊测试和验证。你能在写完代码之后说一句“我会把输出重定向到文件用diff校验结果”面试官对你的工程素养评价会明显高一层。6.2 自旋锁在真实项目里的生存空间把自旋锁写进业务代码我个人非常不推荐。业务逻辑的临界区通常不是简单几行指令可能是数据库操作、RPC调用、文件读写这种长临界区一旦自旋CPU浪费是灾难性的。但自旋思想在底层框架里很常见。JUC里很多并发容器和工具类都用了自旋加CAS的组合比如一些并发队列的入队出队、FutureTask的状态流转。这些场景的特点是临界区极短就是几个字段的比较和赋值几十纳秒。当临界区这么短时线程挂起再唤醒的开销反而比重试几次CAS更大自旋成了更经济的选择。HardCore版的自旋用在这里也算一个教学案例它让你理解为什么JDK内部会设计自适应自旋JVM会根据过去一段时间自旋是否成功来动态调整自旋次数避免无意义的空转。6.3 泛化思考3个线程变成N个线程M遍如果想把这道题吃透我建议你顺手做一个泛化版本N个线程循环打印M遍顺序依次是线程0、线程1、线程2……线程N-1然后回到线程0。硬刚版思路直接改成state % N idSemaphore版做成一个数组每个线程acquire自己的信号量然后release下一个Condition版用循环创建N个Conditionsignal下一个下标。抽象出来之后你会发现这三种做法本质上是同一个逻辑的不同表达判断“谁当前拥有令牌”以及“把令牌交给谁”。这种泛化能力很值钱。面试时你说出“我还能把三线程扩展成N线程”比死记硬背三线程代码要有说服力得多。我个人在实际操作中的体会是这道题最好的学习方式不是抄一遍标准答案而是把三种方案都写一遍、都跑一遍、都故意改错一遍。改错的过程才是真正长本事的过程。比如你亲手把volatile删掉看一次卡死把while改成if看一次乱序把Semaphore的release目标写错看一次死锁——以后再遇到类似问题你脑子里会立刻浮现出“这场景我见过”的直觉。最后再分享一个小技巧写完这类并发Demo别直接看控制台输出先准备一个期望串比如ABC.repeat(10)然后把程序输出重定向到文件用diff一比对秒出结论。这个习惯帮我省下的时间早就超过写这篇总结花的时间了。

相关新闻

ERP进销存源码二次开发指南:环境搭建、库存扣减与权限模块避坑

ERP进销存源码二次开发指南:环境搭建、库存扣减与权限模块避坑

简介:这是一套面向中小企业开发者与Java学习者的开源ERP进销存系统源码,围绕采购、销售、库存与财务等核心业务,帮助读者理解企业资源计划系统的完整实现思路。压缩包共1448个文件,约14.26MB,以png、gif、jpg等界面素材…

2026/10/15 15:46:02 阅读更多 →
告别选择困难!Cursor vs Kiro深度测评,看完就知道选哪个

告别选择困难!Cursor vs Kiro深度测评,看完就知道选哪个

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

2026/10/15 15:46:02 阅读更多 →
STM32编码器测速代码实战:正交解码、M/T法与溢出处理

STM32编码器测速代码实战:正交解码、M/T法与溢出处理

简介:这份STM32编码器测速代码面向嵌入式初学者、电赛控制类参赛者及电机控制开发者,围绕STM32定时器的编码器模式,解决增量编码器信号采集与电机转速精确测量问题。资源包共184个文件,以34个c源文件、35个h头文件、34个o目标文件…

2026/10/15 15:45:01 阅读更多 →

最新新闻

Pycopy:存储优先的Python方言,让嵌入式与云端部署更轻量

Pycopy:存储优先的Python方言,让嵌入式与云端部署更轻量

简介:Pycopy是一个极简、轻量且可高度扩展的Python方言实现,源自MicroPython,面向桌面端、云端与微控制器等各类受限环境。该源码压缩包适合嵌入式及物联网开发者,也适合想理解精简解释器设计的程序员。项目实现了完整的Python 3.…

2026/10/15 16:25:22 阅读更多 →
Meta开放式机架V3:BBU与机架结构设计深度解析

Meta开放式机架V3:BBU与机架结构设计深度解析

聊聊数据中心里的机架,可能很多人下意识想到的就是机房那一排排黑乎乎的铁柜子,标准19英寸、某个高度、前后通风,设备一层层往上摞。但如果你真的在一线维护过超大规模集群,你一定会对传统机架有一肚子槽点:电源线乱七…

2026/10/15 16:25:22 阅读更多 →
软件模拟ADS1220驱动:GPIO实现24位ADC采样,无需硬件SPI

软件模拟ADS1220驱动:GPIO实现24位ADC采样,无需硬件SPI

简介:面向STM32F103xx系列微控制器的开发者,这份软件模拟ADS1220采样芯片的驱动方案,适用于仪器仪表、工业控制等需要高精度模拟信号采集的各类测控场景。压缩包共172个文件,大小5.22MB,以C语言源码为主体,…

2026/10/15 16:25:22 阅读更多 →
软件模拟SPI驱动ADS1220:GPIO手搓24位ADC采样方案

软件模拟SPI驱动ADS1220:GPIO手搓24位ADC采样方案

简介:面向STM32F103xx开发者的ADS1220软件模拟驱动包,以纯软件方式模拟SPI时序,完成高精度ADC的初始化、工作模式配置、数据转换与结果读取,解决无硬件SPI或低成本采集场景下的模拟量输入需求。ADS1220为24位低功耗模数转换器&…

2026/10/15 16:25:22 阅读更多 →
HTML转PDF实战:无头浏览器选型、打印样式与避坑指南

HTML转PDF实战:无头浏览器选型、打印样式与避坑指南

简介:这是一套基于Aspose.Pdf库实现HTML转PDF的.NET示例工程,面向Web开发、文档生成等领域需要稳定生成PDF的技术人员。压缩包共32个文件,大小约10.62MB,涵盖C#源码、可直接引用的DLL、PDF演示文件、Web.config等配置,…

2026/10/15 16:25:22 阅读更多 →
Binder通信机制深度解析:从内存映射到线程模型,一次讲透IPC性能优化

Binder通信机制深度解析:从内存映射到线程模型,一次讲透IPC性能优化

1. 从一个"为什么调用这么慢"的疑问说起如果你做过一段时间的系统层开发,或者维护过跨进程通信频繁的模块,大概率遇到过这样的场景:一个看似简单的接口调用,耗时却比预期高出一个数量级;日志里明明只打了几行…

2026/10/15 16:24:20 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →