并发编程三大特性实战:原子性、可见性、有序性及volatile全解析

做并发编程的人,迟早会遇到这么一种情况:代码在自己机器上跑得好好的,一上多线程就像中了邪,数据不是多了就是少了,程序偶尔还卡死在某个看似无限循环的地方。我在某电商项目做库存扣减时就撞上过类似的事——并发量一上来,库存对不上账,翻日志查到凌晨都没定位到问题,最后才发现是传统的"并发三大特性"没守住。所谓并发三大特性,指的是原子性(Atomicity)、可见性(Visibility)和有序性(Orderliness)。这是并发编程的基本功,也是排查线上问题时的核心框架,每次遇到莫名其妙的并发Bug,我都习惯按这三条逐项套一遍,基本不会落空。这篇文章不打算照着课本念定义,我想从实际踩过的坑出发,把这三条特性掰开揉碎讲清楚,配合代码和排查思路,适合刚接触多线程的初学者,也适合写了好几年单线程、一碰并发就头疼的同行。

1. 并发问题的起点:一次事故背后的三条线索

1.1 库存对不上账,问题到底出在哪个环节

先说那起库存事故。业务逻辑本身很简单:用户在商品页确认下单,后端判断库存大于0,减掉库存,生成订单。伪代码长这样:

if (stock > 0) { stock--; createOrder(); }

单线程下这段代码没有任何问题,但压测环境一开,两个线程同时进了if判断,都看见stock等于1,于是都执行了减库存,最后库存变成0,订单却生成了两笔。这就是典型的超卖。

当时我第一反应是"是不是幂等没做好"或者"数据库事务隔离级别配错了",查来查去都不是。后来把这段代码的字节码拉出来看才发现,问题比想象中底层——一行stock--在字节码里根本不是一条指令,而是"读值、减一、写回"三个动作。线程在执行完第一步后被切换走,另一个线程插进来照原样执行,数据就这么错了。

1.2 三大特性是应对谁的"组合拳"

那次排错之后,我把并发问题的根源整理了一下,发现它们都跑不出三个来源:线程切换、CPU缓存、指令重排序。并发三大特性,恰好就是分别应对这三板斧的。

  • 原子性问题,根源在线程切换。线程随时可能被切走,一段看起来连贯的操作,内部只要分成若干步骤,就可能被其他线程插队。
  • 可见性问题,根源在每个CPU都有多级缓存。线程修改了变量,数据可能还留在自己的缓存里,没有及时写回主内存,其他线程读到的就是旧值。
  • 有序性问题,根源在编译器和CPU会为了性能调整指令执行顺序。单线程内怎么调结果都一样,一旦多线程共享变量,调整后的顺序就可能暴露中间状态。

我用银行柜台打过一个比方:多个窗口各自维护一小本账,往里存钱先记在自己的小本上,等某个时间点再统一归总到总账。你要是只知道往自己窗口的小本上写了金额,隔壁窗口查总账自然查不到最新的。并发三大特性要解决的,就是"小本什么时候归总、谁先谁后、排队怎么排"这档子事。

2. 原子性:你以为的一条语句,其实是三次握手

2.1 从字节码看 i++ 的幕后

原子性(Atomicity)的定义并不复杂:一个操作要么全部执行成功且中途不被任何因素打断,要么完全不执行。但难就难在,绝大多数人分不清"看起来是一步"和"实际上是几步"。

我拿最经典的count++来说。很多人写多线程计数器都默认这行代码是原子操作,其实完全不是。用javap -c拆开看,count++在字节码层大概是这么四条:

getstatic // 读取 count 的当前值 iconst_1 // 常量1入栈 iadd // 加法操作 putstatic // 把结果写回 count

一条高级语言语句,对应四条字节码指令。线程在这四条指令之间的任意位置都可能被操作系统切走。假设线程A执行完 getstatic,读到 count=5,然后被切走;线程B也进来执行完整的count++,把 count 改成6;这时候线程A恢复,继续执行 iconst_1、iadd、putstatic,结果还是把6写回去。两次自增,最终只加了1,更新就这么丢了。

2.2 天生原子和容易翻车的操作

那哪些操作天然原子?在主流JVM里,基本类型赋值(除了long/double)通常是原子的,比如int a = 1;、boolean flag = true;,这类操作在底层就是一次简单的写操作,线程切换不会把它拆开。对象引用赋值在32位和64位JVM上也都能保证原子写。

容易踩坑的是这三类:

  • 读改写复合操作:i++、count--、x = x * 2这类,先读旧值再算新值再写回,三步之间处处可被插队。
  • 检查再行动操作:if (stock > 0) { stock--; },判断和执行是两个独立步骤,判断完之后、执行之前,共享状态可能已经被别人改了。
  • 64位数据在32位虚拟机上的读写:long和double会被拆成两次32位写入。线程A写高32位,线程B写低32位,写出来可能是一个高32位来自A、低32位来自B的混合值。这属于比较冷门但确实存在的坑。

2.3 保证原子性的三种武器

实践中保证原子性,我常用的有三种方式,各有适用场景:

方案原理适用场景缺点
synchronized / Lock给临界区加锁,保证同时只有一个线程进入代码块较长、涉及多个变量、需要事务感阻塞式,锁竞争激烈时吞吐下降
原子类 AtomicInteger、AtomicLong、AtomicReference底层使用CAS,比较并交换,失败就重试单变量计数、状态标记、序列号生成只能在单个变量上保证原子性,多变量联动就无能为力
局部变量 / 不可变对象不共享可变状态,自然不存在竞争能设计成无状态就无状态需要重构代码,不是所有业务都适用

我个人的习惯是:能用局部变量就不提共享变量;必须共享且只是计数,优先用原子类;涉及多个共享变量联动,老老实实上锁,别用CAS硬凑。CAS虽然无锁,但多变量一致性没法靠它保证,硬凑反而会在业务层捅出更大的篓子。

2.4 常见误区:volatile 不是原子性的钥匙

这里必须单独拎出来说清楚一件事:volatile 不保证原子性。很多初学者以为给变量加上 volatile 就能解决并发计数问题,这是并发三大特性里最典型的认知误区。

public class Counter { private volatile int count = 0; public void incr() { count++; // 看起来有volatile保护,实际上还是读改写三步 } }

这个incr()方法即使 count 声明成 volatile,也解决不了丢更新的问题。因为 volatile 只保证"读到的一定是最新的值"和"禁止指令重排",但它管不到"读取->计算->写回"这个复合流程。线程A读到了最新的5,线程B也读到了最新的5,两人都算完写回6,照样丢一次更新。真正要避免这种丢更新,要么用synchronized,要么用AtomicInteger。把 volatile 用在计数器上,属于把"可见性"当成"原子性"用,这是并发编程新手最容易犯的错误,没有之一。

3. 可见性:内存里的修改,为什么对方就是看不到

3.1 工作内存与主内存的分工

可见性(Visibility)要解决的问题是:线程A改了共享变量,线程B能不能立刻看到。

先看Java内存模型(JMM)怎么规定的。JMM里有一个主内存(Main Memory),所有共享变量都放在主内存;每个线程还有一份自己的工作内存(Working Memory),线程对变量的所有读写都得先在工作内存里操作,再同步回主内存。这个"工作内存"其实是CPU缓存加寄存器等概念的综合抽象,不是真实存在的独立空间。

也就是说,线程A执行flag = false,修改的是自己工作内存里的副本;如果这个副本没有及时刷回主内存,线程B在主内存里看到的 flag 仍然是 true。刷回时机不由程序员控制,由虚拟机和硬件决定。这跟银行柜台的小本账一模一样:你办了存款,柜员在自己小本上记一笔,总账还没更新,隔壁窗口查余额当然查不到。

3.2 用代码复现一个"停不下来"的死循环

我刚工作那会儿,被一个问题卡了整整一天:一个后台任务线程在某些机器上死活不退出。代码精简之后大概长这样:

public class VisibilityDemo { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { while (flag) { // 空转 } System.out.println("工作线程退出"); }); worker.start(); Thread.sleep(1000); flag = false; System.out.println("flag 已被主线程修改"); worker.join(); } }

我预想主线程睡一秒后把 flag 改成 false,工作线程就会退出。实际运行呢?在某些JDK版本、某些机器上,工作线程就是一直在空转,程序永远不结束。原因就是上面说的可见性问题:主线程把 flag 的新值写进了自己的工作内存,没有立刻同步到主内存,工作线程每次循环都在读自己的工作内存副本,拿到的永远是旧值 true。

给flag加上 volatile 之后,程序就正常退出了:

private static volatile boolean flag = true;

3.3 volatile 的可见性原理:从写缓存到缓存一致性

volatile 为什么有效?它给读写操作加了两条语义:

  • 写 volatile 变量时:JMM 会强制把新值立即写回主内存,并让其他CPU核心里对应的缓存行失效。
  • 读 volatile 变量时:JMM 会强制从主内存重新读,或重新从总线获取最新值,而不是直接复用本地缓存的旧副本。

底层依赖的是 CPU 的缓存一致性协议,比如常见的 MESI 协议。大致流程是:核心A修改了 volatile 变量,通过总线发出消息,让其他核心的缓存行标记为失效;其他核心一旦要读这个变量,发现缓存行失效,就得重新从主内存拉取。这套机制相当于给"小本账"加了一条硬性规定:改完账立刻广播给所有窗口,谁再查余额都必须用最新总账。

不过,这里要提醒一下:volatile 保证的是"一个线程写、其他线程读"的可见性,不等于所有并发问题都解决了。如果两个线程同时去写同一个 volatile 变量,该出现的竞争还是会出现,这一点和第2节说的原子性问题要连起来理解。

3.4 不只是 volatile:synchronized 和 Lock 也刷可见性

很多人以为 synchronized 只负责互斥,其实它同时解决了可见性。JMM 规定:线程在释放锁之前,会把自己工作内存里的修改强制刷新到主内存;另一个线程获取锁之后,会强制重新从主内存读取共享变量。也就是说,锁的进入和退出附带了一次内存屏障。

Lock 接口(比如 ReentrantLock)走的是同样的内存语义,lock和unlock同样伴随工作内存的刷新。所以我排查可见性问题时,除了看变量有没有加 volatile,还会顺带看这段代码是否已经被锁保护。如果读写都在锁内,可见性通常不缺,缺的是别的东西。反过来,如果一个共享变量没有加 volatile 也没有锁保护,而你又明确希望修改对其他线程立即可见,那它很大概率就是隐患源。

4. 有序性:代码不会说谎,但执行顺序会

4.1 重排序的三个来源

有序性(Orderliness)是关于执行顺序的问题。我写代码时理所当然认为,第一行先执行,第二行后执行。但编译器和CPU为了挖掘性能,会悄悄调整指令的执行顺序——只要调整之后,单线程内的结果还是符合语义的。

重排序主要有三个来源:

  • 编译器优化重排序:编译器觉得自己能证明两条语句不互相影响,就可能在生成指令时交换它们的顺序。
  • 处理器指令级并行重排序:现代CPU有多条执行流水线,计算单元和加载单元是并行的,后一条计算指令可能先于前一条加载指令完成。
  • 内存系统的重排序:CPU读写缓存和写缓冲器的时机不完全一致,看起来像是先访问了内存的某一块,实际先落到了另一块。

单线程环境下,重排序永远不会暴露问题,因为重排序的前提就是"结果不能变"。多线程环境下就危险了:一个线程的中间执行状态,可能被另一个线程看在眼里,而这个状态并不符合代码书写顺序。

4.2 经典翻车现场:双重检查锁

有序性最著名的翻车案例就是双重检查锁(DCL)单例。当年大家为了让单例既延迟加载又高效,写出过这样的代码:

public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查,无锁 synchronized (Singleton.class) { if (instance == null) { // 第二次检查,有锁 instance = new Singleton(); } } } return instance; } }

看起来逻辑很严密:先无锁判断,避免大多数情况进锁;进锁后再检查一次,防止两个线程同时通过初审。但问题恰恰出在new Singleton()这行代码。在字节码层面,创建一个对象大致分三步:

  1. 分配内存空间;
  2. 在内存上初始化对象,构造方法执行;
  3. 把instance引用指向这块内存。

单线程内,顺序是 1->2->3 不会错,但重排序可能让实际执行变成 1->3->2:先分配内存,把引用暴露出去,再回头执行构造方法。多线程下会出现什么?线程A执行到步骤3,instance已经不是 null 了,但对象还没初始化完;线程B通过第一次检查发现instance != null,直接拿去用,读到的就是一个半成品对象,轻则字段全是默认值,重则直接抛异常。

4.3 volatile 与内存屏障

解决办法很直接:给instance加上volatile。volatile 的另一个核心能力就是禁止指令重排序,靠的是内存屏障(Memory Barrier)。

内存屏障是处理器层面的一组指令,用来强制规定前后指令的内存操作顺序。常见的有四种:

屏障类型作用
LoadLoad屏障前的读操作先于屏障后的读操作完成
LoadStore屏障前的读操作先于屏障后的写操作完成
StoreStore屏障前的写操作先于屏障后的写操作完成
StoreLoad屏障前的写操作先于屏障后的读操作完成

volatile 在实现上,会在写操作前后插入 StoreStore 和 StoreLoad 屏障,在读操作前后插入 LoadLoad 和 LoadStore 屏障,从而把重排序限制在一个安全范围内。DCL单例里的instance = new Singleton()就不会被重排成先暴露引用再初始化对象了。

这里插一句我的经验:现在不少资料建议直接用静态内部类或者枚举实现单例,DCL确实越来越像考古题。但DCL作为理解有序性的教学案例,价值非常大,因为它的失败原理和线上很多诡异问题是一模一样的——不是代码逻辑错,是对象初始化的中间状态被别人看见了。

5. happens-before:并发世界的红绿灯

5.1 六条规则速览

学了三大特性之后,还有个问题摆在面前:我总不能在每个共享变量上都加 volatile 吧?那代码没法看了。JMM 给出了更系统的规则——happens-before 关系。它定义了一套"如果A操作 happens-before B操作,那么A的执行结果对B可见,且A的执行顺序先于B"的判定规则。我平时排查并发问题,先不急着加关键字,先拿这套规则推演一遍代码。

常用的规则有这些:

规则内容
程序次序规则同一个线程内,按代码书写顺序,前面的操作 happens-before 后面的操作
锁规则解锁操作 happens-before 后续对同一把锁的加锁操作
volatile 变量规则对一个 volatile 变量的写 happens-before 之后对该变量的读
传递性如果A happens-before B,B happens-before C,则A happens-before C
线程启动规则线程的 start() 调用 happens-before 该线程内的任何操作
线程终止规则线程内的所有操作 happens-before 其他线程检测到该线程终止(如 join() 返回)

以锁规则举例:线程A执行完 synchronized 块,释放锁之前把共享变量都刷回主内存;线程B随后进入同一个 synchronized 块,由于锁规则,B 不仅获得了互斥权,也继承了A在这之前所有对共享变量的修改。所以锁既是互斥工具,也是内存同步工具。

5.2 为什么有了 volatile 还需要锁

一个常见的问题:既然 volatile 有完整的写读 happens-before 规则,为什么很多场景还得上锁?

因为 happens-before 只能保证"看得到最新的值",保证不了"多个操作合在一起是原子的"。举个直观例子:多个线程同时对 volatile 变量做sum += x,即使每个线程都能读到最新的 sum,也可能因为读改写三步的交错,丢掉中间某次累加。可见性管不到复合操作的整体性,原子性才管得到。所以遇到"多个共享变量一起操作""先判断再修改"这类场景,还是要靠锁把整个临界区串行化,锁能同时覆盖原子性和可见性两重需求。

5.3 用 happens-before 推演一次线上异常

之前排查过一个消息队列消费者的问题:消费者线程从内存队列取任务,处理完后把统计值写到共享 Map;另一个监控线程每秒读一次 Map 做报表。偶尔报表数据比实际少一条。

推演过程是这样的:

  • 任务处理线程写共享 Map,发生在释放某把锁之前;
  • 监控线程也通过同一把锁进入临界区读取 Map,锁规则成立,理论上下一次读取应该能看到最新值;
  • 但我看了代码,监控线程并没有加锁,只是普通读取。

于是锁规则在这里根本不成立,监控线程能读到什么,取决于 CPU 缓存刷回主内存的时机,完全随机。这就解释了"偶尔少一条"的现象。修复方案有两种:监控线程也加同一把锁;或者把 Map 换成支持并发读的数据结构,再把写操作做原子化。用 happens-before 规则一推演,问题症结一目了然,比靠猜快得多。

6. 实战复盘:一个订单号生成器的三重修复

6.1 第一版:唯一性全凭运气

前面讲了不少理论,最后用一个实际项目里的小工具串一遍。当时某系统要做分布式订单号,要求同一时刻不能重复,我上来的第一版很天真:

public class OrderNoGenerator { private static int sequence = 0; public static String nextOrderNo() { return "ORDER-" + System.currentTimeMillis() + "-" + (sequence++); } }

时间戳加自增序号,看起来每个订单号都不一样。压测刚开始就出问题:同一毫秒内多个线程拿到的 sequence 相同,订单号直接重复。

6.2 用三大特性逐条对照找病根

我没急着改代码,而是拿并发三大特性逐条去对照:

  • 原子性:sequence++是"读-改-写"复合操作,多线程并发时更新会丢。
  • 可见性:sequence没有 volatile 也没有锁,线程B修改后线程A不一定看得到,进一步加重重复。
  • 有序性:这个场景下重排序虽然不直接导致重复,但System.currentTimeMillis()和sequence++之间如果乱序,长串订单号的排序性和可读性都会受影响。

结论很清楚:根子在原子性和可见性,尤其是原子性。

6.3 修正版本与压测对比

修正方案我试了两种。第一种简单粗暴,直接加 synchronized:

public class OrderNoGenerator { private static int sequence = 0; public static synchronized String nextOrderNo() { return "ORDER-" + System.currentTimeMillis() + "-" + (sequence++); } }

第二种用 AtomicInteger,无锁但保证原子性:

public class OrderNoGenerator { private static final AtomicInteger SEQUENCE = new AtomicInteger(0); public static String nextOrderNo() { return "ORDER-" + System.currentTimeMillis() + "-" + SEQUENCE.incrementAndGet(); } }

压测对比结果如下(单机8线程,每秒生成订单数):

方案线程数是否出现重复平均响应
第一版:int + 自增8是较快,但不可用
第二版:synchronized8否吞吐下降约30%
第三版:AtomicInteger8否接近第一版

如果业务上还要兼顾日期格式、机器标识和随机段,我会在第三版基础上加日期格式化段。AtomicInteger 在单实例内部完全够用;如果订单号要跨实例生成,就得引入分布式ID方案,那又是另一个话题了。

6.4 写并发代码前的自查清单

把这次复盘提炼成一张自查清单,我写并发相关的共享变量时会反复对照:

  1. 这个操作是原子的吗?如果中间有读改写或者先判断后执行,必须上锁或用原子类。
  2. 这个变量需要立刻被其他线程看到吗?需要就加 volatile,或者保证读写都在同一个锁内。
  3. 有没有依赖代码书写顺序?如果有,原顺序重排后会不会暴露中间状态?需要 volatile 或锁来限制重排。
  4. 这组共享变量之间有没有联动?有联动就不要用单个原子类硬撑,直接设计一把锁保护整组变量。

每次写完并发代码,我都会问自己这四句话。能答清楚,说明三大特性的功课到位了;答不清楚,多半未来线上会给你上一课。

最后再分享一个小体会:面试时很多人能把三大特性名字背得滚瓜烂熟,但一问"volatile 能不能保证原子性"就卡住,一让排查并发问题就只会加锁。其实三大特性不是三个孤立的考点,它们是一套排查框架。遇到并发Bug,先判断是读改写丢更新还是读旧值,再看是不是执行顺序被重排了,按这个思路走,绝大多数问题都能迅速定位。写代码之前用这套框架过一遍大脑,比事后熬夜查日志要划算得多。

相关新闻

机器人第一性原理:从物理约束推导系统设计

机器人第一性原理:从物理约束推导系统设计

做机器人开发时间长了,我有个很深的感受:大部分项目的失控,不是败在某个算法写不出来,而是从最开始就没想明白“这个系统到底需要满足什么物理条件”。这其实就是所谓的机器人第一性原理——不照着别人的配置单抄,不从…

2026/10/20 15:34:27 阅读更多 →
REA业务建模:用资源、事件、代理重构订单与库存系统

REA业务建模:用资源、事件、代理重构订单与库存系统

最近我在整理一个内部项目的时候,给它的代号起了个简短的名字:REA。如果你也经常被业务模型绕晕,这个缩写大概能给你打开口子——REA 在这里不是某个前端框架,也不是某个公司内部系统代号,而是 Resource-Event-Agent 的…

2026/10/20 15:34:27 阅读更多 →
从知道到能交付:技能拆解与刻意练习的完整指南

从知道到能交付:技能拆解与刻意练习的完整指南

每次更新个人主页或简历时,最头疼的往往不是“项目经历”那一栏,而是“技能”这一栏。写得太宽泛,比如“熟悉办公软件”“掌握编程基础”,自己也觉得心虚;写得太细碎,又像是往脸上贴金,生怕面试…

2026/10/20 15:34:27 阅读更多 →

最新新闻

手写鼠标点击粒子特效:从原理到性能优化的完整实战

手写鼠标点击粒子特效:从原理到性能优化的完整实战

前阵子给某活动页面做氛围优化,需求方反复强调要有“互动感”。白底、黑字、产品图,排版再干净,鼠标移上去没有任何反馈,也确实容易让人产生“这是个静态PPT”的错觉。于是我想到了“鼠标粒子特效”——准确说,是一段H…

2026/10/20 21:16:52 阅读更多 →
Java实现IP归属地查询:离线库方案与Spring Boot封装实践

Java实现IP归属地查询:离线库方案与Spring Boot封装实践

在做日志分析和用户画像时,后端服务拿到一个公网IP,最朴素的需求就是想知道这个IP到底来自哪个省、哪个市,甚至哪个运营商。这个需求听起来不难,网上搜“Java IP归属地查询”会跳出一堆在线API,复制一段代码就能跑通&a…

2026/10/20 21:16:52 阅读更多 →
基于SpringBoot和微信小程序的考研学习系统开发实战

基于SpringBoot和微信小程序的考研学习系统开发实战

先说结论:这套“基于微信小程序的考研学习系统”,选题方向是对的,技术栈也不冷门。后端SpringBoot、前端微信原生小程序,功能覆盖学习计划、每日打卡、题库刷题、错题本、学习统计和后台管理。作为计算机毕业设计,它兼…

2026/10/20 21:16:52 阅读更多 →
context-mode上下文模式设计:从状态机建模到工程落地的完整实践

context-mode上下文模式设计:从状态机建模到工程落地的完整实践

1. 从“上下文模式”这个命名说起:它到底在解决什么问题第一次看到“context-mode”这个词,很多人会下意识地把它当成某个框架里的配置项,或者某个编辑器插件的开关。但如果你真的在工程一线待过几年,就会发现这个词背后藏着一个非…

2026/10/20 21:16:52 阅读更多 →
STM32F103裸机驱动两相四线步进电机:时序精度与IO翻转控制实战

STM32F103裸机驱动两相四线步进电机:时序精度与IO翻转控制实战

简介:本资源是一套基于STM32F103微控制器的两相四线步进电机驱动程序,面向嵌入式初学者、电机控制实践者及自动化设备开发工程师,解决步进电机精准启停、方向切换与细分控制等核心问题,适用于3D打印机、CNC小车、精密位移平台等需…

2026/10/20 21:16:52 阅读更多 →
Java+Selenium自动化测试入门:核心机制与工程实践全解析

Java+Selenium自动化测试入门:核心机制与工程实践全解析

1. 项目概述:JavaSelenium到底在解决什么问题说实话,Selenium这个框架我已经用了快十年,从最早的Selenium IDE到现在的WebDriver 4.x,踩过的坑能装满一个仓库。这些年有很多新手问我同一个问题:"JavaSelenium到底…

2026/10/20 21:15:44 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50: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/20 9:50:45 阅读更多 →