新闻详情

新闻详情

首页 / 资讯中心 / 详情

线程安全核心原理与常见坑:C++、Java、C#多线程编程实战指南

发布时间:2026/9/11 4:25:02来源:尧图网络
线程安全核心原理与常见坑:C++、Java、C#多线程编程实战指南
写多线程代码十个新手九个会在某个深夜被一个诡异的问题折磨到怀疑人生数组下标越界了、统计数据莫名变少、程序偶尔崩溃又偶尔正常。翻日志看执行顺序明明每一步都没问题结果就是不对。不懂线程安全的人看到的是“灵异事件”懂的人一眼就知道共享数据被多个线程同时动了而且没有做防护。线程安全说白了就一句话多个线程在同一段时间里去访问同一份数据还能保证结果正确不翻车、不串味、不互相覆盖。这个听起来简单但真做起来坑太多。今天我想把线程安全这套东西从头到尾捋一遍并且从 C、Java、C# 三个语言的视角都带你看一遍因为这三兄弟的写法不同、坑也不同但底层思路完全一致。这篇内容适合刚接触并发编程的菜鸟也适合被多线程 bug 折磨过但一直没时间系统梳理的朋友。1. 先搞懂线程安全到底在说哪件事1.1 线程安全问题的根源三个条件凑齐才会出事很多刚入门的人会把“线程安全”当成一个玄学概念好像多线程代码天生就容易出问题。其实不是线程安全问题是有触发条件的。要出问题必须同时满足三件事第一有多个线程在并发执行第二这些线程访问了同一个数据第三这个数据不是只读的而是会被修改。三个条件缺一个线程安全问题基本不会发生。怎么理解呢我举个生活化的例子。一个办公室有两个人一起维护一张报销登记表。如果这两个人只是轮流看表那完全没冲突。但如果两个人同时拿到同一张表一个在表格里填自己的报销金额另一个也在同一行上填自己的金额最终表格里只能留下一个人的记录另一个人的数字就被覆盖了。这就是线程不安全的本质多个执行流同时在一个位置上“读改写”结果互相踩踏。反过来如果每个线程只操作自己的局部变量例如 C 里函数内部的 int iC# 里的局部 ListJava 里的局部 HashMap那么不管创建多少个线程它们之间没有任何交集自然不会不安全。如果数据创建之后从来不修改只被多个线程读那通常也是安全的因为读同一个值不会改变它。所以你判断一段代码是否线程安全最先要做的就是找“共享可变状态”哪些数据是被多个线程共同访问、而且会发生写操作的。找不到这个就谈不上线程安全问题。1.2 为什么会存在“非原子操作”这种坑明确了“共享可变状态”之后第二个关键概念就是“原子操作”。原子操作的意思是一个操作要么完整执行完要么等于没执行不存在执行到一半被另一个线程看到的中间状态。听起来很基础但问题在于很多你以为是原子的操作底层其实是拆成好几步的。最典型的例子就是计数器的自增操作。不管是 C 的 count、Java 的 count还是 C# 的 count看代码就像一步操作但到了 CPU 指令层面它实际上要执行三步把 count 从内存读到寄存器在寄存器里加 1再把结果写回内存。三个步骤中间任意一个节点都可能被其他线程插进来。回到报销单的例子A 线程读到当前金额是 100准备加 50还没来得及写回去B 线程也读到了 100准备加 30B 先写完变成了 130A 再写又变成 150。两个人一共应该加 80但最终记录只加了 50另一个人做的修改完全被覆盖了。菜鸟最容易想不通的就是这一点代码明明执行了 count为什么 count 的值不对因为 count 不是一个单独动作而是“读、改、写”三个动作的组合这个组合在并发环境下并不是密不可分的。1.3 原子性、可见性与有序性并发编程的三大基础问题光理解“非原子操作”还不够线程安全问题其实可以细分成三个维度原子性、可见性、有序性。原子性就是刚才说的“不可分割性”。加锁、原子类本质上都是在保护原子性保证一个操作序列在并发下不被打断。可见性指的是一个线程改了共享变量的值另一个线程能不能立刻看到最新的值。你可能会觉得这还用问吗内存是共享的呀。但现实是为了性能CPU 会有缓存编译器也会做优化线程可能一直在读自己缓存里的旧值根本没发现数据已经被其他线程修改了。经典案例就是 Java 里一个线程改了一个 boolean 标志位另一个线程的 while 循环却永远跳不出去。有序性就是代码的执行顺序可能被编译器或 CPU 重排。比如你写代码是先给变量 a 赋值再给变量 b 赋值但实际执行顺序可能是先 b 后 a。在单线程环境下重排序不影响最终结果但多线程环境下另一个线程可能看到“b 已经变了、a 还没变”这种中间状态从而做出错误判断。这三大问题不是某一种语言独有的C、Java、C# 都有一套内存模型来定义这些行为但内核都是围绕“原子性”“可见性”“有序性”展开的。后面单例模式那一节就是这三个问题最经典的案例现场。2. 单例模式是理解线程安全最好的教材2.1 从懒汉单例说起菜鸟最容易写错的代码单例模式可能是讲解线程安全最经典、最完整的素材没有之一。不是因为单例本身有多复杂而是因为它把“共享可变状态”和“非原子操作”完美地结合在了一起。所谓懒汉式单例就是第一次使用时才创建实例。以 Java 为例很多新手第一次写出来都是这个版本public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { instance new Singleton(); } return instance; } }这段代码在单线程下没有任何问题。但一旦两个线程同时第一次调用 getInstance()就出事了线程 A 检查到 instance 为 null准备 new线程 B 也检查到 instance 为 null也准备 new。最终可能产生两个截然不同的实例并且分别被不同的线程拿走了。单例不单了。C 的懒汉写法也一样class Singleton { public: static Singleton* getInstance() { if (instance nullptr) { instance new Singleton(); } return instance; } private: static Singleton* instance; };这种“先检查再创建”的代码本质上就是前面说的读改写三步操作检查是读new 是写中间没有加任何同步所以必然不安全。2.2 C 里更稳的姿势局部静态变量与 call_once既然懒汉裸写不安全很多人第一反应是加锁。C 里最简单的加锁方式是这样#include mutex class Singleton { public: static Singleton getInstance() { std::lock_guardstd::mutex lock(mutex); if (instance nullptr) { instance new Singleton(); } return *instance; } private: static Singleton* instance; static std::mutex mutex; };这个写法确实线程安全了但问题是每次调用 getInstance() 都要加锁哪怕实例已经创建好了后面所有的调用都是纯读操作加锁成本就太高了。于是有人想到了双重检查锁Double-Checked Locking Pattern缩写经常是 DCLP先不加锁检查一次如果为空再加锁加锁之后再检查一次。static Singleton getInstance() { // 第一次检查不加锁为了性能 if (instance nullptr) { std::lock_guardstd::mutex lock(mutex); // 第二次检查加锁之后为了安全 if (instance nullptr) { instance new Singleton(); } } return *instance; }双重检查锁的思路是对的但在 C11 之前这个写法还有个隐藏问题instance new Singleton() 这行不是原子的它包含“分配内存”“构造对象”“把地址赋给 instance”三个步骤。如果 CPU 重排了先把地址赋给 instance再执行构造那么另一个线程在第一次检查时就会看到 instance 不为空然后直接返回一个还没构造完成的对象使用时就崩了。这也是为什么菜鸟背了双重检查锁的模板还是出 bug 的原因之一。C11 之后要想彻底解决这个问题最简单的办法之一是用函数局部静态变量class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } };C11 标准保证函数局部静态变量的初始化是线程安全的第一个线程进入初始化时其他线程会等待初始化完成。编译器会在背后自动帮你加同步逻辑而你不需要写一行锁代码。如果你确实需要延迟创建并且想在代码里显式控制C11 还提供了 std::once_flag 和 std::call_onceclass Singleton { public: static Singleton getInstance() { std::call_once(flag, init); return *instance; } private: static Singleton* instance; static std::once_flag flag; static void init() { instance new Singleton(); } };std::call_once 保证回调函数只会被一个线程执行一次其他线程会阻塞到初始化完成为止。相比自己手写双重检查锁这个方案在正确性上要稳得多也是我在实际项目中更推荐的方式。2.3 Java 双重检查锁为什么还要加 volatileJava 的懒汉单例同样有双重检查锁的写法但很多人抄模板时少了一个关键字结果线程安全还是有问题public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }这里的关键在于 instance 必须用 volatile 修饰。为什么因为 new Singleton() 不是一步完成的JVM 在堆上分配内存、调用构造器、把对象引用写入 instance这几个步骤在指令层面可能被重排序。如果没有 volatile线程 A 可能在对象构造完成之前就把引用赋值给了 instance线程 B 在第一次检查时看到 instance 不为 null直接拿去用得到一个没有完全初始化的对象。volatile 在这里的作用不只是保证可见性还通过内存屏障禁止了构造过程中的重排序相当于给“发布操作”加了一道隔离墙。这正是我在 1.3 节提到的“有序性”问题在实际代码中的体现。当然如果你不是在学习或者面试现实中 Java 写单例更推荐用枚举或者静态内部类。C# 也有类似的现成工具就是 Lazy public class Singleton { private static readonly LazySingleton lazy new LazySingleton(() new Singleton()); public static Singleton Instance lazy.Value; }Lazy 默认是线程安全的内部已经处理好了初始化时的并发问题。菜鸟阶段无需纠结它内部是怎么实现的先知道“不要在并发环境下手写双重检查锁”就够了。3. 集合与容器的线程安全从 C# List 线程安全聊起3.1 List 不是线程安全的实测给你看热搜词里经常有人搜“c#线程安全list”说明这个坑被踩过的人非常多。答案很简单C# 的 List 不是线程安全的Java 的 ArrayList 不是线程安全的C 的 std::vector 同样不是线程安全的。为什么不是因为它们内部为了性能几乎不提供任何同步机制。多个线程同时往一个 List 里 Add 元素看起来只是在集合尾部追加数据但内部呢可能要扩容分配一块更大的新数组把旧元素拷贝过去再添加新元素。如果两个线程同时触发扩容就会出现一个线程正在拷贝另一个线程已经在新数组上写入了最终结果可能是抛 ArgumentOutOfRangeException可能是 Count 不对也可能是数据静默丢失。我用 C# 做过一个很简单的实验开 8 个线程每个线程往同一个 List 里加 1000 次元素结束后看 List 的 Count。理论上应该是 8000但实际跑出来的值经常是 7900 多偶尔还会直接抛出异常。这就是典型的“多个线程同时修改共享可变状态”。Java 的 ArrayList 同理多个线程并发 add 时轻则丢数据重则数组越界。C 的 std::vector 并发 push_back同样存在未定义行为因为标准库容器本身压根不考虑多线程同时修改同一实例的情况。3.2 更靠谱的替代方案lock、并发容器、不可变设计C# 里面想要一个线程安全的“list”有几个思路各有各的适用场景。第一个思路是给所有访问加锁private readonly object listLock new object(); private Listint list new Listint(); public void Add(int item) { lock (listLock) { list.Add(item); } }这个方案的优点是简单、任何 List 方法都能包缺点是粒度粗如果读多写少全用同一把锁并发读也会互相阻塞性能上限不高。如果你想要多个读者并发读、写者独占可以考虑 ReaderWriterLockSlim或者直接用不可变集合的快照机制。第二个思路是直接换并发容器。C# 里没有 ConcurrentList 但有 ConcurrentBag 、ConcurrentQueue 、ConcurrentDictionaryK,V 等。ConcurrentBag 是无序集合专门适合生产者和消费者线程混用场景如果对顺序有要求用 ConcurrentQueue。它们内部用了细粒度锁或无锁算法并发性能比给 List 包一层大锁好很多。第三个思路是从设计上消灭“共享可变”两个字。比如每个线程处理完数据之后把结果合并到一个新集合中或者在发布阶段才把只读集合暴露出去。C# 里有 ImmutableList 每次修改都会生成新实例旧实例再也不会变天然线程安全。Java 里也有 java.util.Collections.unmodifiableList 和 CopyOnWriteArrayList。CopyOnWriteArrayList 的名字已经说明了一切每次写操作都会复制整个数组代价高但读操作完全无锁适合读多写极少、数据量不大、遍历频繁的场景例如事件监听器的存储。3.3 不同语言常见线程安全容器对比很多人学多线程时只盯着自己那一门语言看到 C 的“线程安全容器”还要琢磨半天其实只要你把容器分类的思想理顺换语言也就是查表的事。C 标准库本身没有提供线程安全的容器实现std::vector、std::map 这些都不安全。大多数场景的做法是自己用 mutex/shared_mutex 包一层或者引入 Intel TBB 这类并行库里面有 concurrent_vector、concurrent_queue、concurrent_hash_map。选型时要考虑序列化开销shared_mutex 适合读多写少但写独占时吞吐量会下降。Java 的并发展现相对丰富很多ConcurrentHashMap 是并发场景下的默认选择CopyOnWriteArrayList 适合读多写极少BlockingQueue 系列ArrayBlockingQueue、LinkedBlockingQueue天然适合生产者-消费者模型。另外 java.util.concurrent 包下还有各种具备并发语义的队列和集合。C# 这边System.Collections.Concurrent 命名空间下提供了 ConcurrentDictionary、ConcurrentQueue、ConcurrentStack、ConcurrentBag。BlockingCollection 可以给并发容器包一层阻塞边界适合做简单的工作队列。做一个简单的对照表场景C 常用方案Java 常用方案C# 常用方案通用集合std::mutex std::vector/std::mapConcurrentHashMapConcurrentDictionary生产者-消费者队列TBB concurrent_queue 或自定义BlockingQueue/LinkedBlockingQueueBlockingCollection ConcurrentQueue读多写少的并发读集合std::shared_mutex 保护CopyOnWriteArrayList / ReadWriteLockReaderWriterLockSlim List无锁计数或简单累加std::atomicAtomicInteger / LongAdderInterlocked我个人的经验是用并发容器别为了“并发”两个字而过度设计。数据量小、并发低的时候一把锁包住普通容器代码最简单也最难写错。一旦出现锁竞争导致的性能瓶颈再考虑用专用并发容器替换不要一上来就上最重的方案。4. 线程安全的层级你能用的工具其实分好几层4.1 原子操作与原子类最轻量的一层但不是万能的线程安全的手段不是只有锁它其实分了好几个层次。最轻量的一层是原子操作。C11 以后有 std::atomic 模板C# 有 Interlocked 静态类Java 有 java.util.concurrent.atomic 包。以计数器为例用锁可以做但用原子操作更轻、更快也更容易理解std::atomicint count{0}; // 线程安全的自增 count.fetch_add(1);int count 0; // 线程安全的自增 Interlocked.Increment(ref count);AtomicInteger count new AtomicInteger(); count.incrementAndGet();原子操作解决的问题是“把读改写三步变成一步”所以单独一次自增、单独一次加锁交换都是安全的。但原子操作解决不了“复合操作”的问题。什么意思比如你先判断某个原子变量是不是 0如果是 0 就自增不是 0 就返回错误。这看起来是两次原子操作拼起来的但两个操作之间依然有可能被其他线程插进去整体结果还是可能不符合预期。这也是一个很常见的菜鸟误区用 volatile 修饰 int然后在多线程里自增以为安全了。volatile 只能保证可见性不能保证原子性。C 的 volatile、Java 的 volatile、C# 的 volatile都不保证自增是原子的。真正想保证自增原子性要么用原子类要么加锁。4.2 锁最通用但最容易用错的一层当原子操作搞不定复合逻辑时就要上锁。锁的本质是“互斥”同一时间只允许一个线程进入临界区其他线程在外面等着。C 常用 std::mutex std::lock_guard 或者 std::unique_lockC# 用 lock 语法糖Java 用 synchronized 关键字或者 ReentrantLock。三种语言的写法千差万别但核心逻辑完全一样。锁用错的表现也有很多最常见的三个第一个锁的不是同一个对象。Java 里 synchronized 加在实例方法上锁的是 this加在静态方法上锁的是 Class 对象。如果你一个方法用了 synchronized 实例方法另一个方法用了 synchronized 类对象那它们互不相干不能被同一把锁保护。C# 的 lock 也一样锁的对象必须是同一个引用才能起到互斥作用。第二个给局部变量加锁。有人把锁对象定义在方法内部每次调用都创建一个新的 lock 对象自然没有互斥效果。正确做法是锁对象必须是所有访问线程共享的同一个实例一般作为类的私有字段存在。第三个锁内执行耗时操作。加锁只保护共享数据访问就够了结果有人把网络请求、文件读写、Sleep 全都放在锁里面导致其他线程长时间阻塞并发性能直线下降。锁粒度是一个需要反复权衡的点粒度太粗并发度低粒度太细又容易因为多个锁而产生死锁或者一致性问理。我一般的策略是优先用一把锁保护一个不变量集合如果发现锁竞争变成瓶颈再考虑用细粒度锁或者无锁数据结构而不是一开始就搞一堆锁。4.3 线程封闭与不可变性从根上消灭线程安全问题线程安全还有一个更优雅的思路就是干脆不让多个线程共享同一个可变数据。这个思路叫“线程封闭”。线程封闭的意思是数据只能被一个线程访问即使这个线程内部随便改也不会被别人看到。最典型的实现是局部变量每个线程的方法栈是独立的函数里创建的局部变量天然线程封闭不需要做任何同步。在 Java 里还有一个专门的工具 ThreadLocal每个线程都会得到独立的一份变量副本。C 有 thread_local 关键字C# 有 ThreadStaticAttribute 和 AsyncLocal。它们共同的特点是看起来像一个共享变量但每个线程访问到的都是自己那份数据从设计上就避免了竞争。另一个思路是“不可变性”。数据一旦创建就不变了任何修改都生成新对象那么所有线程都只能读旧对象读旧对象不会改变它的值自然线程安全。Java 的 String、C# 的 string、C 的 const 对象都是这个思想的体现。C# 里的 ImmutableList、ImmutableDictionaryJava 里用 final 字段配合不可变类设计都是把一个可变集合变成发布后不可修改的版本。线程封闭和不可变性不能解决所有问题但确实是性价比最高的方案。因为锁和原子操作只是让你在竞争激烈的环境中“安全地踩刹车”但根本的拥堵来源是“多个线程都想改同一个东西”。如果能够把一个可变对象限定在一个线程内或者设计成不可变那么整个并发模型就会简单非常多。4.4 伪线程安全看着安全实际一戳就破菜鸟很容易犯的另一个问题是误判自己已经用了“线程安全”的接口于是就不再考虑同步。最典型的例子是 ConcurrentDictionary 的 GetOrAdd 方法。ConcurrentDictionary 本身是线程安全的容器它的 GetOrAdd 可以保证返回的 key 在字典里只有一个。但它不保证“创建 value 的工厂函数只执行一次”。看这段 C# 代码var dict new ConcurrentDictionaryint, string(); string value dict.GetOrAdd(1, key ExpensiveCreate(key));多个线程同时调用 GetOrAdd(1, ...)时ExpensiveCreate 是有可能被执行多次的最终只有一个结果会被写进字典但那些重复执行造成的浪费是不可忽略的。如果你的 value 制作过程有副作用比如写文件、发请求那这个“线程安全”接口也会给你带来大麻烦。再比如 Java 的 ConcurrentHashMap 的 computeIfAbsent 同样存在类似语义。文档写得明明白白但菜鸟经常不会细读。我的建议是任何并发容器的“复合操作”语义都要以官方文档为准不要凭直觉。还有前面提到的“先判断再执行”逻辑。有人写if (list.Count 100) { list.Add(item); }就算这里不直接用 List 而用 ConcurrentQueue也不能保证安全因为 Count 和 Add 是两个独立操作中间可能被另一个线程插进来。真正的安全写法是把整个“判断操作”放到同一个锁里或者使用并发容器专门提供的原子方法比如 ConditionalWeakTable、TryAdd 之类的语义。5. 菜鸟最容易踩的坑和排查经验5.1 死磕锁粒度加锁范围太大不行太小也不行锁粒度的问题是并发编程里最难把握的经验题之一。加锁范围太大典型表现是方法上直接 synchronized或者 lock 包裹整个方法体。这样做的优点是简单、不容易漏缺点是并发能力会很差。如果你的业务逻辑里有耗时的 I/O 操作那其他线程全都会堵在锁外面系统吞吐量直线下降。加锁范围太小典型表现是只锁住了修改的那一行但读取的地方完全没有锁。比如一个线程在写 list你用 lock 包住了 add但另一个线程读 list.Count 或者遍历 list 时没有 lock。这会导致读写操作失去互相排斥的关系读线程可能看到写线程改建到一半的数据结构轻则脏读重则崩溃。我在项目里一般会先画出“临界区”的范围只需要保证共享状态在“读改写”这个完整行为中不被干扰那么就把这个整体行为放进锁里而不是只锁其中一句。I/O、网络、Sleep 这种耗时操作尽量移出锁外。如果耗时操作必须和共享状态绑定可以考虑把共享数据先复制一份出来在锁外处理处理完再用锁合并结果。5.2 排查线程安全问题的思路和工具多线程 bug 难排查是因为它不像普通 bug 一样稳定复现。我遇到过线上偶现的问题本地跑几百次都不出现一上线就随机冒出来。排查的第一步是复现。不要指望一次就能稳定复现而是要在代码里增加并发压力比如加大循环次数、增加线程数、把 Sleep 随机插入到关键路径上。如果问题在高压测试下出现的概率提高了那就基本可以锁定是并发问题。第二步是记录现场。用日志输出线程 ID、时间戳、关键变量的值。上下文信息越全越容易定位是哪两个线程在竞争同一个资源。在 C 中可以考虑用 ThreadSanitizer简称 TSan做动态检测它是编译器内置的工具能非常准确地报告数据竞争发生在哪一行代码。GCC 和 Clang 都支持 -fsanitizethread。在 Java 中jstack 可以打印线程栈jvisualvm 可以看线程竞争情况。C# 可以用 Visual Studio 的并发可视化器Concurrency Visualizer或者用 dotnet-trace 抓事件。第三步是审视共享状态。拿出一张纸列出所有被多个线程访问的共享变量然后在每个变量旁边写出“靠什么保护”是无条件不可变是 volatile是原子类是锁如果哪个变量旁边是空的那它很可能就是 bug 的根源。5.3 常见问题速查表遇到症状直接翻这里很多菜鸟遇到问题最怕的不是不知道答案而是不知道怎么描述问题。下面这张表是我在实际工作和带新人过程中整理的高频问题对照可以直接照着排查。症状可能原因处理方向数组或 List 操作报下标越界多个线程同时修改集合内部数组扩容或移动换并发容器或对修改操作统一加锁计数结果偶尔比预期少count 不是原子操作多个线程覆盖写入用原子类或 Interlocked明明加了锁还是出错锁对象不是同一个实例或锁范围没包住读取代码统一使用同一个私有锁对象并覆盖所有读写路径程序卡死性能突然归零锁内执行了耗时操作或出现了长时间自旋把 I/O、等待移出临界区缩短持锁时间两个线程互相等待永远不往下走死锁多个锁的获取顺序不一致统一锁的获取顺序或用超时锁 avoid使用 volatile 自增但结果还是不对volatile 不保证原子性自增仍是读改写换成原子类或锁单例 mode 实例被创建了多次懒汉模式没有同步保护使用语言级安全单例C 局部静态变量 / Java 枚举 / C# Lazy用了 ConcurrentDictionary GetOrAdd 但耗时函数反复执行并发容器的复合方法不保证工厂只执行一次预创建对象或接受重复创建并用 index 判断这张表不是标准答案却是我从实际调试里一条一条攒出来的。遇到问题先套一下大部分情况都能缩小排查范围。5.4 分享一个我自己一直用的检查习惯在我自己的项目里写并发代码之前我会先做三件事画共享状态图标注保护机制再写并发测试。画图不是为了好看而是为了逼自己想清楚每个共享变量到底靠什么保证安全。标注保护机制是盯着代码逐行检查每次访问共享变量有没有锁定有没有用原子类有没有保证发布时安全写完代码之后我会写一个简单但压力足够的并发测试脚本让几十个线程同时访问核心方法跑几十万次。如果测试能稳定通过才敢认为这段代码在大多数场合不会出问题。如果测试偶尔失败那就证明问题真实存在一定要追到根因不能靠“重启大法”跳过。还有一个写代码层面的小习惯尽量把并发控制封装在一个类内部不要让调用方自己处理锁。对外提供的公共方法本身就是线程安全的内部用什么手段是内部细节。这样做能大大减少外部误用锁对象的概率。最后再说一个很多老手都认可的经验能不用锁就不用锁能少共享就少共享。线程安全最理想的状态不是把锁玩出花而是代码结构天然就不需要那么多锁。你先试着用线程封闭、不可变对象和并发容器去解决大部分问题只有剩下的、绕不开的临界区才用锁保护。这样做出来的系统不仅正确性容易保证调起性能问题来也会省心很多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

9DVR帽椅:沉浸式科普体验的技术解析与应用 2026/9/11 5:04:07

9DVR帽椅:沉浸式科普体验的技术解析与应用

1. 9DVR帽椅:重新定义沉浸式科普体验 在科技馆的角落里,一群孩子正戴着造型奇特的"帽子",身体随着画面不断倾斜转动,时而发出惊呼,时而开怀大笑。这不是什么魔法道具,而是最新一代的9DVR帽椅——…

阅读更多 →
W55MH32跑小智聊天机器人:嵌入式语音交互开发实战 2026/9/11 5:04:07

W55MH32跑小智聊天机器人:嵌入式语音交互开发实战

前阵子我把手头一个桌面小音响改造成了能聊天的语音助手,主控用的是 W55MH32,软件底座是社区里很火的小智聊天机器人项目。折腾了大概三周,踩了七八个坑,最后总算达到“喊一声就应答、闲聊不尬住”的状态。这篇文章就围绕这套组合…

阅读更多 →
嵌入式开发板完整使用流程:从串口调试到Qt部署 2026/9/11 5:04:07

嵌入式开发板完整使用流程:从串口调试到Qt部署

1. 开发板不是“插电就能跑”的玩具,而是嵌入式开发的最小完整系统 很多人第一次拿到开发板,第一反应是接上USB线、打开串口终端、敲个 ls ——然后发现什么都没输出,或者卡在U-Boot界面不动。我刚入行那会儿也这样,以为开发板和…

阅读更多 →
Linux解压命令从tar到7z:核心用法、算法选型与工程避坑指南 2026/9/11 5:04:07

Linux解压命令从tar到7z:核心用法、算法选型与工程避坑指南

这两年帮人排查过不少线上事故,发现一个很有意思的现象:很多人在 Linux 上解压文件靠的是肌肉记忆——看到 .tar.gz 就 tar -zxvf,看到 .zip 就 unzip,遇到 .7z 当场懵住。装 JDK、pnpm、RocketMQ 这类中间件,文档第一…

阅读更多 →
Novu自托管如何配置JWT_SECRET、STORE_ENCRYPTION_KEY等关键密钥 2026/9/11 5:04:07

Novu自托管如何配置JWT_SECRET、STORE_ENCRYPTION_KEY等关键密钥

Novu自托管如何配置JWT_SECRET、STORE_ENCRYPTION_KEY等关键密钥 【免费下载链接】novu The open-source communication infrastructure for agents and products 项目地址: https://gitcode.com/GitHub_Trending/no/novu 用 Docker Compose 自托管 Novu 时,…

阅读更多 →
CMSIS-FreeRTOS源码深度解析:架构、隐式依赖与工程避坑指南 2026/9/11 5:01:06

CMSIS-FreeRTOS源码深度解析:架构、隐式依赖与工程避坑指南

1. 项目概述:为什么CMSIS-FreeRTOS值得你花三天时间逐行读完它的源码我第一次在STM32F407上跑通CMSIS-FreeRTOS的hello world时,以为自己已经“掌握”了RTOS。直到半年后,一个电机控制任务在高负载下出现毫秒级的调度延迟,中断嵌套…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞