先看一段看起来很简单的代码。inta0;booleanflagfalse;// 线程 Aa10;flagtrue;// 线程 Bif(flag){System.out.println(a);}假设线程 B 在 A 执行完flag true之后才进入分支此时它读到的flag是true。那么它打印出来的a一定是 10 吗不一定有可能是 0。这里要先说明一个前提上面假设a和flag是共享变量比如类的字段。如果它们是方法里的局部变量每个线程各有一份根本不存在共享问题。明明 A 先执行、B 后执行为什么 B 看不到 A 的修改这不是代码写对了语言却出错而是我们写代码时漏考虑了并发场景下的两类前提。这两类前提正好对应两种并发问题。一、并发问题的两类根源1.1 第一类CPU 缓存与指令重排序这类问题里线程甚至不需要同时改同一个变量只要一个线程写、另一个线程读就可能读到旧值。根因有两个CPU 缓存写操作先落在本地缓存没及时写回主内存另一个线程读到旧副本 →可见性问题指令重排序写操作的实际生效顺序和代码顺序不一致另一个线程先看到结果、后看到前提 →有序性问题。对着开头的代码逐行看inta0;// ① 共享变量初始值 0booleanflagfalse;// ② 共享变量初始值 false// 线程 Aa10;// ③ A 修改 aflagtrue;// ④ A 修改 flag// 线程 Bif(flag){// ⑤ B 检查 flagSystem.out.println(a);// ⑥ B 读取 a}③ 和 ④ 在 A 的本地内存里执行什么时候写回主内存并不保证。如果 ③ 一直没写回⑤ 即使读到了flag true⑥ 读到的a仍是 0 →可见性。③ 和 ④ 没有数据依赖可能被重排成 ④ → ③。B 在 ④ 之后、③ 之前进入分支⑥ 读到的a仍是 0 →有序性。单看每一行都没问题问题在于 ③④ 和 ⑤⑥ 之间没有建立任何先后可见的约束。本篇讲这一类也就是 JMM 和 volatile 要解决的问题。另一类线程交错放到 1.2只做简单铺垫。1.2 第二类多线程交错执行多个线程同时读写同一份共享状态一个读-改-写的操作可能被打断。publicclassVolatileAtomicityDemo{publicstaticvolatileintinc0;// 共享计数器publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServicepoolExecutors.newFixedThreadPool(5);// 5 个工作线程for(inti0;i5;i){pool.execute(()-{for(intj0;j500;j){inc;// 每个线程执行 500 次}});}pool.shutdown();pool.awaitTermination(1,TimeUnit.MINUTES);// 等所有任务结束System.out.println(inc);// 期望 2500}}对着代码看newFixedThreadPool(5)创建 5 个工作线程每个线程循环 500 次执行incinc不是一行原子操作而是读inc→ 加 1 → 写回inc三步。两个线程可能同时读到同一个旧值各自加 1 后写回同一个结果中间的更新就丢了。5 × 500 本该是 2500实际往往更小。这里即使把inc声明成volatile结果也可能不对volatile 只保证每次读拿到最新值不保证读-改-写三步整体不被打断。这类问题属于原子性问题靠锁、CAS、原子类解决下一篇专门讲。需要说明的是这类问题的本质是线程交错不只是多核并行。哪怕单核 CPU 靠时间片轮转两个线程的指令照样可能交错结果一样会错。类型触发条件破坏的特性典型例子解决方向缓存与重排副本未同步、指令换序可见性、有序性状态标志、DCL 单例volatile、锁线程交错多线程同时读写共享状态原子性i、先检查后操作锁、CAS、原子类二、根因一CPU 缓存导致看不见第一类问题的根因有两个CPU 缓存和指令重排序。这一节先把缓存讲透下一节再讲重排。2.1 缓存的作用这里的周期不是抽象单位而是CPU 时钟周期它代表时间。以 3 GHz 主频为例一个周期约 0.33 ns。CPU 执行一条简单指令通常只要几个周期访问主内存却要一百多个周期差了两个数量级。把周期换算成时间更直观层级归属典型容量典型延迟周期大致时间3 GHzL1每个核心私有32–64 KB4–5 个周期约 1.5 nsL2每个核心私有512 KB–2 MB10–20 个周期约 3–7 nsL3多核共享16–96 MB40–60 个周期约 13–20 ns主内存所有核心共享16–128 GB100 个周期约 33 ns 以上也就是说CPU 等一次主内存的时间足够执行几十条指令。缓存就是为了弥补这段速度差把经常访问的数据放在更快、更小的地方让 CPU 尽量不用等。这层关系其实和 Redis 缓存 MySQL 很像把经常访问的数据放在更快、更小的地方减少访问慢速存储的次数。现代处理器通常就是三级缓存结构如下为什么是三层这是速度、容量、成本的折中越靠近核心做得越快但容量小、成本高越远离核心容量大、便宜但慢。每往下一级容量大约提升一个数量级延迟也明显增加。层级太少缓存命中率不够层级太多布线和缓存一致性维护的成本又上来了收益递减。所以主流处理器落在三级但这不是硬性规定部分处理器还有 L4。另外L1 通常分成指令缓存和数据缓存所以物理上看起来像四块逻辑上仍然是三级。2.2 缓存的工作方式与窗口期CPU 读数据时先把包含目标数据的一整块缓存行通常 64 字节复制到自己的缓存之后需要用时直接读缓存。运算完成后再把结果写回缓存什么时候同步到主内存由缓存一致性协议和写策略决定。这就带来一个问题每个核心都可能持有一份共享变量的副本。拿i来说两个线程同时执行。先看图再看逐时刻的状态表时刻线程 A线程 B主内存 it1从缓存读到 i11t2从缓存读到 i11t3计算 112写回2t4计算 112写回2正确结果应该是 3实际却是 2。两个线程都基于旧值计算后写的覆盖了先写的。这段读完之后、写回之前的时间就是常说的窗口期。CPU 用缓存一致性协议比如 MESI缓存行有 Modified / Exclusive / Shared / Invalid 四种状态来保证副本之间最终能保持一致一个核心改了数据会让其他核心的对应副本失效下次读时重新取。但要说清楚MESI 解决的是副本一致不是复合操作原子。它能让别的核心看到你写的新值却不能阻止两个核心同时完成读-改-写这三步。可见性和原子性是两件不同的事。三、根因二指令重排序3.1 什么是指令重排序指令重排序是指编译器、JIT、CPU 在不改变单线程程序执行结果的前提下调整指令实际执行顺序的行为。代码里写的顺序是程序员视角的顺序指令真正执行的顺序可能和它不一样。举个最简单的例子inta1;// 1intb2;// 2intcab;// 31 和 2 没有数据依赖可以先执行 2 再执行 1也可以让两条指令重叠执行3 依赖 1、2 的结果不可能排到它们前面。指令能否被提前原因int b 2;能可以排到a 1;前面和 a 没有数据依赖int c a b;不能排到 a、b 前面依赖 a、b 的结果单线程之所以感觉不到是因为as-if-serial语义不管怎么重排单线程的执行结果必须和按代码顺序执行一样。换句话说重排可以改变过程但不能改变单线程看到的结果。3.2 指令重排序的作用重排不是为了捣乱是为了让 CPU 少等。现代 CPU 是流水线作业一条指令要经过取指、译码、执行、访存、写回等阶段理想情况下每个阶段都排满。但只要遇到依赖或慢操作比如访问主内存流水线就会出现空泡。重排就是把后面不依赖当前结果的指令提前填上这些空泡。具体到三类重排编译器优化重排编译器在不改变单线程语义的前提下调整语句顺序。比如int a 1; int b 2;两条赋值没有依赖编译器可以先安排 b 再安排 a它还会做常量折叠、公共子表达式消除等优化重排只是其中一种。指令级并行重排ILP现代处理器有多条流水线可以同时执行多条指令。只要没有数据依赖a 1和b 2可以重叠执行CPU 等内存时也可以先把后面独立的指令拿出来跑。内存系统重排CPU 写数据时先写进 store buffer写缓冲区不立刻落主内存读数据时也可能走 load buffer。于是“先写后读”在程序里是顺序的到了内存系统里可能变成“读先于写生效”。这不是 CPU 乱来而是为了不让写操作阻塞后续指令。举个例子做饭时等水开你会先去切菜而不是站在锅边等。重排就是 CPU 的“等水开时先切菜”单线程的最终结果不变但整体时间被利用起来了。当然重排有边界有数据依赖的指令不能换序比如c a b不能排到a、b前面。单线程靠 as-if-serial 保证结果多线程就不一定了这就是 3.3 要讲的问题。3.3 多线程下重排序为什么会出问题问题在于编译器只对单线程负责。它不知道你写flag是为了通知另一个线程也不知道另一个线程会依赖a和flag的先后顺序。回到开头的代码a10;flagtrue;这两条写没有数据依赖编译器或 CPU 完全可能让flag true先生效。线程 B 看到flag为true进入分支读到的a却还是 0。这不是编译器故意捣乱它只是在单线程视角下做了合法优化。多线程的语义需要我们人为地用规则约束。这套用来约束多线程下可见性与有序性的规范就是 JMM。四、JMM多线程下要遵守的内存规范4.1 JMM 是什么为什么需要它JMM 全称Java Memory Model即 Java 内存模型。先纠正一个常见误解它不是JVM 运行时数据区堆、栈、方法区也不是把内存条切成主内存区和工作内存区。它是 Java 语言定义的一组并发编程规范。为什么 Java 要自己定义一套因为不同操作系统、不同 CPU 的内存模型不一样。x86 比较强ARM 比较弱如果直接复用操作系统的内存模型同一套代码换个平台就可能跑出不同结果。Java 要跨平台就必须自己抽象一套统一的规则屏蔽底层差异。这套规则主要做两件事抽象线程与主内存之间的关系规定从 Java 源代码到 CPU 指令的转化过程中哪些重排允许、哪些必须禁止。对开发者来说好处是不用直接和硬件打交道用volatile、synchronized、Lock这些关键字和类就能写出并发安全的程序。4.2 主内存与本地内存JMM 把所有内存抽象成两块主内存所有线程共享存放共享变量比如实例字段、静态字段、数组元素本地内存规范里常叫工作内存每个线程私有存放该线程读过的共享变量的副本。它涵盖了缓存、写缓冲区、寄存器以及编译器优化后的临时存储是一个抽象概念并不真实存在。方法里的局部变量、形参、方法返回值不在线程间共享所以不属于 JMM 说的共享变量也不受这套规则约束。注意局部变量本身不共享但它引用的对象可能分配在堆上被其他线程共享。线程只能操作自己本地内存里的副本不能直接访问别的线程的本地内存。举个具体的例子线程 A 把a改成 10想传给线程 B可以把这个过程理解成两步这只是帮助理解的模型不代表硬件真的按这个顺序执行线程 A 把本地内存中修改过的a副本同步到主内存线程 B 到主内存读取a的最新值。但要注意JMM 不是运行时的保姆它只负责定规则不会自动帮你维护可见性。规则是 JMM 定的代码是你写的只有你按规则写JMM 才保证结果符合预期。具体来说你得用volatile、synchronized、start/join这些手段在写和读之间建立 happens-before 关系。关系建立了JVM 才保证写的结果对读可见没有建立JMM 就允许底层按优化后的方式执行。拿开头的代码举例flag是普通变量时A 的写和 B 的读之间没有 hbJMM 就允许 B 读到 0把flag改成volatile后写和读之间有了 hbJMM 才保证 B 看到a 10。4.3 八种交互操作JMM 定义了八种操作描述变量如何在主内存和本地内存之间来回操作作用对象含义lock主内存变量标记为线程独占unlock主内存变量解除锁定其他线程才能锁read主内存变量把变量的值传输到本地内存load本地内存变量把read得到的值放进副本use本地内存变量把副本的值交给执行引擎assign本地内存变量把执行引擎得到的值赋给副本store本地内存变量把副本的值传回主内存write主内存变量把store传回的值写入主内存变量配套还有几条规则最常用的几条是read和load必须成对出现store和write必须成对出现不允许丢弃最近的assign也就是改了副本不能当没改变量只能在主内存被lock同一时刻只能有一个线程锁住它对一个变量执行lock会清空本地内存中该变量的副本执行unlock前必须把修改同步回主内存。4.4 happens-before判断谁对谁可见的尺子JMM 需要在程序员和编译器之间找平衡程序员想要一个强内存模型按规则写代码就能理解结果编译器和处理器想要一个弱内存模型尽量少受约束好做优化。happens-before 就是这条平衡线如果一个操作 A happens-before 另一个操作 B那么 A 的执行结果对 B 可见并且 A 的执行顺序排在 B 之前。注意这里的之前是可见性顺序不要求底层真的按这个顺序执行。只要重排后的结果和按 happens-before 执行的结果一致JMM 就允许重排。intuserNumgetUserNum();// 1intteacherNumgetTeacherNum();// 2inttotalNumuserNumteacherNum;// 31 happens-before 2、2 happens-before 3、1 happens-before 3。虽然 1 在 2 前面但 1 和 2 换序不影响结果所以 JMM 允许3 依赖 1、2所以 3 必须排在后面。常用的 happens-before 规则有六条程序顺序同一线程内代码前面的操作 happens-before 后面的操作监视器锁对同一把锁的unlockhappens-before 后续的lockvolatile对同一个 volatile 变量的写 happens-before 后续的读传递性A happens-before BB happens-before C则 A happens-before C线程启动Thread.start()happens-before 新线程里的每一个动作线程终止线程里的所有动作 happens-before 其他线程从join()返回。4.5 三大特性特性含义谁会被破坏靠什么保证原子性一个操作不可分割要么全做要么全不做i这类读-改-写复合操作synchronized、CAS、原子类可见性一个线程修改后另一个线程能看到CPU 缓存、写缓冲区volatile、synchronized、final有序性程序按预期顺序执行不被随意重排编译器、CPU 重排序volatile、synchronized、happens-before现在再回头看开头A 的写和 B 的读之间没有任何 happens-before 关系所以 JMM 允许 B 读到旧值A 的两条写之间也只需要满足单线程语义所以允许flag先生效。问题不在代码写错了语法而在我们没给这两段代码建立可见性与顺序上的约束。五、volatile把可见 有序落地5.1 volatile 在 JMM 里的位置volatile 是 JMM 提供的一个关键字JMM 规定它应该有什么语义JVM 负责把语义映射成具体 CPU 指令和内存屏障。它只保证两件事可见性、有序性。原子性不归它管。5.2 可见性写要刷出读要拿最新值对 volatile 变量写assign之后必须立刻storewrite把新值写回主内存读use之前必须readload每次都要拿主内存的最新值。这里的立刻是语义效果不是说硬件真的跳过缓存直接读内存。CPU 仍然会走 L1/L2/L3真正起作用的是 MESI 缓存一致性协议 volatile 带来的内存屏障写完之后让其他核心的副本失效读之前确保拿到最新值。5.3 有序性四种内存屏障volatile 通过内存屏障禁止重排序。屏障是一条栅栏前面的访存先完成后面的访存不能提前越过去。屏障示例保证什么LoadLoadLoad1; LoadLoad; Load2Load1 的读先于 Load2 及其后续读完成StoreStoreStore1; StoreStore; Store2Store1 的写先于 Store2 完成并对其他处理器可见LoadStoreLoad1; LoadStore; Store2Load1 的读先于 Store2 及其后续写完成StoreLoadStore1; StoreLoad; Load2Store1 的写对其他处理器可见且先于 Load2 及其后续读StoreLoad 的开销是四种里最大的它同时约束前后所有访存常被称为全能屏障Full Barrier。volatile 读写的插入策略是volatile 写前面插 StoreStore后面插 StoreLoadvolatile 读后面插 LoadLoad 和 LoadStore。这样安排之后volatile 写之前的所有操作对随后读这个 volatile 变量的线程来说都变得可见。这句话不是口号它就是下面 DCL 和 happens-before 示例能成立的原因。5.4 DCL 单例为什么必须加 volatilepublicclassSingleton{privatevolatilestaticSingletonuniqueInstance;privateSingleton(){}publicstaticSingletongetUniqueInstance(){if(uniqueInstancenull){// 第一次检查不加锁synchronized(Singleton.class){if(uniqueInstancenull){// 第二次检查加锁uniqueInstancenewSingleton();}}}returnuniqueInstance;}}new Singleton()在底层是三步分配内存为对象分配一块内存空间初始化执行构造方法把字段赋成业务值引用赋值把这块内存的地址写进uniqueInstance。这里正好回答一个很容易卡住的问题初始化和指向内存到底有什么区别第 1 步做完对象已经存在了但字段还是默认值int是 0引用是nullboolean是false。第 2 步的初始化不是分配内存而是执行构造方法把字段从默认值改成你写的值。做完这一步对象状态才算完整。第 3 步的指向是把对象地址写进引用变量。它是一个发布动作在这之前别的线程没有路径通过uniqueInstance访问这个对象在这之后引用不为null对象就算可见了。打个比方第 1 步是拿到地、建好毛坯房第 2 步是装修第 3 步是把钥匙交给你。如果先交钥匙、后装修你拿到钥匙进门看到的是一间没装修完的房子。new的三步里1 不能排到 3 后面因为 3 要用 1 分配的地址存在数据依赖但2 和 3 之间没有依赖可以重排成1 → 3 → 2线程 A 执行到第 3 步时uniqueInstance已经不为null。线程 B 此时调用getUniqueInstance()第一次检查发现不为空直接返回——但它拿到的对象字段还是默认值是一个半成品。加上 volatile 之后uniqueInstance new Singleton()中的第 3 步成了volatile 写写前面插入 StoreStore挡住第 2 步沉到第 3 步后面写后面插入 StoreLoad保证写完成后其他线程才可能读到。所以线程 B 只要看到uniqueInstance不为null就说明初始化一定已经完成。补充一个实现层面的细节x86 是较强的内存模型TSO硬件层面不允许写-写重排DCL 在 x86 上看起来能跑但编译器 / JIT 仍可能重排而且 JMM 允许这种重排。代码不能依赖某一种 CPU 的运气该加的 volatile 必须加。5.5 volatile 与 happens-before顺带发布普通变量publicclassVolatileHappensBeforeDemo{privateinta0;privateintb0;privatevolatilebooleanflagfalse;// 线程 A 执行publicvoidwriter(){a1;// 1 普通写b2;// 2 普通写flagtrue;// 3 volatile 写}// 线程 B 执行publicvoidreader(){if(flag){// 4 volatile 读intxa;// 5 普通读intyb;// 6 普通读System.out.println(xx, yy);}}}happens-before 链是这样的1、2 happens-before 3程序顺序规则3 happens-before 4volatile 规则4 happens-before 5、6程序顺序规则根据传递性1、2 happens-before 5、6。所以当线程 B 在 4 处读到flag true时5、6 读到的x、y一定是 1、2。volatile 不仅保证flag本身的可见性还通过 happens-before 的传递性顺带保证了它前面普通变量的可见性。这也是实际开发中 volatile 最常见的用法状态标志位。一个线程写标志其他线程读标志标志之前的准备工作对读方可见而且不需要加锁。六、回到开头给flag加上volatilevolatilebooleanflagfalse;线程 A 的a 10与flag true之间是程序顺序flag写与 B 的flag读之间是 volatile 规则B 的读与println(a)之间是程序顺序。传递下来只要 B 看到flag truea就一定是 10。最后用三句话收束缓存让一个线程的修改看不见 → 需要可见性重排让执行顺序对不上 → 需要有序性交错让读-改-写被打断 → 需要原子性下一篇。JMM 是规范volatile 是这套规范里可见性 有序性的一种实现手段。它不签原子性也替代不了锁。记住一句volatile 管看得见、排得对不管算得全。
企业数字化 ERP 产品动态
相关推荐
深入理解BOM与Window对象在前端开发中的应用 1. 为什么需要深入理解Window与BOM 十年前我刚接触前端开发时,曾天真地以为JavaScript就是操作DOM的那点事。直到某天需要实现一个页面跳转功能,在MDN上翻找半天才发现原来window.location才是正解——这个发现让我意识到,BOM(Bro… · 2026/11/3 20:04:39
Polars:大数据处理的高性能替代方案 1. 为什么Polars成为大规模数据处理的新宠?在数据爆炸式增长的今天,传统的数据处理工具开始显得力不从心。作为一名长期与海量数据打交道的工程师,我亲历了从Pandas到Spark再到Polars的技术演进过程。Polars之所以能在短短几年内获得广泛关注… · 2026/11/3 20:04:39
基于Hadoop+Spark的知网文献推荐系统设计与优化 1. 项目背景与核心价值 这个毕业设计项目瞄准了学术文献推荐这一垂直领域,通过整合HadoopSparkHive技术栈构建了一套完整的知网文献处理流水线。我在实际开发中发现,这类系统真正的难点不在于算法本身,而在于如何高效处理知网特有的半结构化数… · 2026/11/3 20:04:39
C语言中goto模拟if-else的原理与实践 1. 为什么用goto模拟if-else?在C语言教学现场,有个学生突然举手问:"老师,既然goto是‘万恶之源’,那为什么教材里还保留这个语法?"这个问题让我想起2012年维护过的某个遗留系统——满屏的goto像蜘… · 2026/11/3 20:39:02
SQL生成大模型选型:定制能力与推理稳定性的平衡及算力配置 SQL 生成这件事,看起来只是"把自然语言翻译成 SQL",但真正落到生产环境里,你会发现它同时被两股力量拉扯:一边是业务方希望模型能懂各种奇奇怪怪的口径、方言函数、历史遗留表结构,也就是定制能力࿱… · 2026/11/3 20:39:02
智慧水务数字孪生平台架构与关键技术解析 1. 智慧水务数字孪生平台的技术架构解析在智慧城市建设的浪潮中,水务管理正经历着从传统人工巡检到智能化预测的转型。我们团队基于孪易IOC(智能运营中心)与GIS(地理信息系统)技术栈,构建了一套"可算可… · 2026/11/3 20:39:02
高并发广告联盟系统架构设计与优化实践 1. 企业级广告联盟APP的核心挑战与架构设计 在移动互联网流量红利逐渐消退的当下,广告联盟作为连接广告主与流量主的关键枢纽,其技术架构直接决定了商业变现效率。我曾主导过日请求量超过5亿次的广告投放系统开发,深刻理解这类系统在实战中面… · 2026/11/3 20:39:02
一条命令搞定部署、备份、健康检查与自动回滚 1. 从一次凌晨三点的回滚说起凌晨三点被电话叫醒的滋味,但凡做过线上运维的人都懂。那次事故的起因特别朴素:一个看似无害的配置改动,把某个服务的健康检查路径写错了,新版本上线之后容器起来了,端口也监听了ÿ… · 2026/11/3 20:39:02
TypeScript Monorepo规范与最佳实践 1. 为什么需要TypeScript monorepo约定?在大型前端项目中,monorepo已经成为管理多包依赖的主流方案。我经历过一个从零搭建的monorepo项目,最初没有制定任何规范,结果三个月后就陷入了配置地狱:不同包的tsconfig互相覆… · 2026/11/3 20:38:02
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46