减少锁竞争与无锁化

减少锁竞争与无锁化

锁竞争的核心代价,来自临界区串行化导致的吞吐量下降、线程阻塞 / 唤醒的内核态切换、CPU 缓存行频繁失效、上下文切换开销 。所有优化方案的核心逻辑,都是从根源减少 / 消除多线程对共享可变资源的并发修改冲突,从 "优化锁开销" 到 "完全不用锁" 分为 5 大类,覆盖从基础编码到架构设计的全层级工程实践。

一、锁本身的优化:降低竞争的强度、范围与时长

这类方案仍使用锁机制,但通过优化锁的使用方式,大幅降低竞争概率与开销,是最基础、落地成本最低的优化手段。

1. 缩小临界区范围

核心原理 :锁的竞争概率与临界区的执行时长正相关,临界区执行时间越短,多线程同时争抢锁的概率就越低。仅把必须互斥访问共享资源的代码 放入锁范围,无关的计算、IO、耗时操作全部移出临界区。
工程实践

  • 避免对整个方法加锁,仅对共享变量读写的核心代码块加锁(如 Java 中避免方法级synchronized,改用代码块级加锁);

  • 严禁在临界区内执行 sleep、网络调用、磁盘 IO 等耗时操作,这类操作会让锁被长期持有,直接放大竞争;

  • C++ 中通过\{\}控制std::lock\_guard的作用域,确保锁在临界区结束后立即释放。
    适用场景:所有使用锁的场景,是优化的第一优先级。

2. 降低锁粒度:分拆锁 / 锁分段

核心原理 :把 1 个全局大锁保护的多个独立资源,拆分为多个小锁分别保护,仅操作同一资源的线程才会发生竞争,把竞争维度从 "全局" 降到 "局部",大幅提升并发度。
工程实践

  • 经典案例:JDK1.7 的ConcurrentHashMap,将哈希表拆分为 16 个Segment,每个 Segment 独立加锁,仅同 Segment 的操作会竞争,并发度较 Hashtable 的全局锁直接提升 16 倍;

  • 数据库场景:用行锁替代表锁,仅锁定待修改的行,而非整张表,大幅降低多事务的冲突概率;

  • 极致优化:属性级锁,一个对象的多个独立字段,分别用独立的锁保护,避免修改不同字段的线程互相阻塞。
    适用场景:共享资源可拆分为多个独立子集、操作可按资源维度隔离的场景。

3. 自旋锁与自适应自旋

核心原理 :线程获取锁失败时,不立即阻塞进入内核态,而是让 CPU 空转(自旋)等待锁释放,避免线程阻塞 / 唤醒的上下文切换开销。自适应自旋则会根据锁的历史竞争情况,动态调整自旋次数:上一次自旋成功则延长自旋时长,上一次自旋失败则直接放弃自旋进入阻塞。
工程实践

  • JVM 内置的synchronized锁优化,轻量级锁阶段默认使用自适应自旋;

  • 内核态 / 高性能场景的自定义自旋锁,如 Linux 内核的自旋锁、DPDK 的自旋锁实现;

  • 注意:仅适用于临界区极短、CPU 核心数充足 的场景,单核 CPU 自旋无意义(同一核心无法在自旋时执行锁持有线程),长临界区自旋会浪费大量 CPU 资源。
    适用场景:锁持有时间极短、并发竞争不极端的高吞吐场景。

4. 偏向锁 / 轻量级锁(JVM 内置优化)

核心原理:针对 "锁大多时候仅被同一个线程持有,无实际竞争" 的真实业务场景,避免重量级锁的内核态切换开销。

  • 偏向锁:首个获取锁的线程,会在对象头记录自身线程 ID,后续该线程再次进入临界区,仅需校验线程 ID,无需 CAS 操作,零开销进入;

  • 轻量级锁:多线程交替使用锁(非同时竞争)时,用 CAS 自旋替代操作系统互斥量,完全在用户态完成,避免内核态切换;

  • 仅当多线程同时竞争锁时,才会升级为重量级锁。
    适用场景:大部分低竞争的业务并发场景,JVM 默认开启。

5. 可重入锁

核心原理 :同一个线程多次获取同一把锁时,无需重新竞争,仅通过计数累加实现重入,避免重复加锁导致的死锁与额外竞争开销。
工程实践 :Java 的ReentrantLocksynchronized,C++ 的std::recursive\_mutex均为可重入实现,典型场景为递归函数内的加锁、嵌套方法调用的加锁。
适用场景:存在嵌套加锁逻辑的场景,避免死锁同时降低锁开销。

二、从根源消除竞争:无共享设计,彻底不用锁

这类方案通过数据隔离,完全消除多线程间的共享可变资源,从架构层面彻底杜绝锁竞争,是最高效的无锁化方案。

1. 线程本地存储(TLS, Thread Local Storage)

核心原理 :为每个线程创建共享变量的私有副本,每个线程仅读写自己的副本,完全不存在跨线程的共享修改,自然无需加锁。
工程实践

  • 语言级实现:Java 的ThreadLocal/FastThreadLocal(Netty 优化版)、C++ 的thread\_local关键字、Linux 的pthread\_key\_t

  • 典型场景:日志框架的 MDC 链路追踪、高并发计数器的线程私有计数、线程私有的缓冲区 / 对象池,避免多线程争抢;

  • 进阶实现:JDK8 的LongAdder,基于 TLS 思想的分段计数,将 value 拆分为多个 Cell,每个线程仅对自己绑定的 Cell 做 CAS,最后汇总结果,高并发下性能远超AtomicLong
    适用场景:变量无需跨线程实时同步、仅需最终汇总的场景,是业务开发中最常用的无锁方案。

2. Share-Nothing(无共享)架构

核心原理 :将系统拆分为多个完全独立的执行单元,每个单元绑定固定的 CPU 核心 / 线程,仅处理自己专属的数据,单元间通过消息传递通信,而非共享内存,从架构层面彻底消除共享可变状态,完全无需锁。
工程实践

  • Actor 模型:Erlang、Akka 的核心架构,每个 Actor 是独立实体,状态仅自身可修改,其他 Actor 只能通过发消息交互,无共享内存,天然无锁;

  • 事件驱动 Reactor 模型:Netty 的NioEventLoop,每个 Channel 绑定固定的单线程 EventLoop,Channel 的所有 IO 与业务逻辑均在该线程串行执行,无并发修改,无需加锁;

  • 高性能网络 / 计算:DPDK 的核心设计,每个 CPU 核心绑定一个独占线程,关闭超线程与调度,线程仅处理本地数据,完全无锁,实现网络包线速转发;

  • 分布式架构:分库分表、Kafka 的分区消费,每个分片 / 分区仅由一个节点 / 线程处理,天然无竞争。
    适用场景:高并发底层系统、分布式系统、网络框架,是架构级无锁化的核心方案。

3. 单线程串行化执行

核心原理 :将所有对共享资源的操作,提交到同一个单线程池串行执行,完全消除并发,自然无需锁。看似牺牲了并行性,但内存操作的耗时极短,单线程即可扛住数十万 QPS,同时完全规避了锁与上下文切换的开销,综合性能远超多线程加锁方案。
工程实践

  • 经典案例:Redis 的核心命令执行模型,单线程串行处理所有命令,完全无锁,凭借内存操作的低延迟,实现超高并发;

  • UI 编程:Android 主线程 Handler、iOS 的 MainRunLoop,所有 UI 操作必须在主线程串行执行,避免多线程并发修改 UI 的问题,无需加锁;

  • 业务场景:将账户操作、订单状态修改等强一致性操作,按用户 ID 哈希路由到固定的单线程串行处理,避免同用户的并发修改,无需加锁。
    适用场景:共享资源操作耗时短、QPS 可被单线程承接的场景,是业务开发中低成本、高收益的无锁方案。

三、无锁并发编程:基于硬件原子原语替代锁

这类方案完全摒弃操作系统锁,依赖 CPU 硬件级的原子指令,实现并发安全的无锁操作,是底层系统开发的核心无锁技术。

核心基础:CAS(Compare-And-Swap,比较并交换)

CAS 是所有无锁编程的基石,是 CPU 硬件级的原子指令(x86 的CMPXCHG、ARM 的LDREX/STREX),全程在用户态执行,无需内核态切换。
核心原理:CAS 包含 3 个操作数 ------ 内存地址 V、旧预期值 A、待更新的新值 B。当且仅当 V 的当前值等于 A 时,才将 V 的值原子更新为 B,否则操作失败,整个过程 CPU 保证原子性,不会被中断。线程 CAS 失败后无需阻塞,可通过自旋重试,实现无锁的并发修改。

1. 原子类

核心原理 :基于volatile保证变量的内存可见性与禁止指令重排序,底层通过 CAS 实现变量的原子读写,完全替代锁,实现无锁的并发修改。
工程实践

  • Java 的java\.util\.concurrent\.atomic包:AtomicIntegerAtomicLongAtomicReference、带版本号的AtomicStampedReference(解决 ABA 问题);

  • C++ 的std::atomic模板库,支持基础类型与自定义类型的原子操作,可通过内存序控制优化性能;

  • ABA 问题解决方案:通过版本号 / 邮戳机制,每次修改变量同时递增版本号,CAS 时同时校验值与版本号,避免 "A→B→A" 的误判。
    适用场景:单个共享变量的原子更新,如计数器、状态标记、引用更新等场景。

2. 无锁数据结构

核心原理 :基于 CAS 与内存屏障,实现链表、队列、哈希表等数据结构的无锁并发访问,入队 / 出队、插入 / 删除、查询操作均无需锁,仅通过 CAS 保证原子性。
工程实践

  • 无锁环形队列:Disruptor 框架的核心,预分配环形数组,通过序列号(sequence)与 CAS 实现无锁的生产消费,性能远超阻塞队列,广泛用于金融、日志等高吞吐场景;Linux 内核的kfifo,用于内核与用户空间的无锁数据传输;

  • 无锁哈希表:JDK1.8 + 的ConcurrentHashMap,放弃分段锁,改用 CAS + 细粒度的头节点锁,空桶节点插入完全通过 CAS 无锁实现,仅哈希冲突时才加锁,大部分场景无锁化;Hopscotch Hash 等完全无锁的哈希表实现;

  • 经典无锁链表:Michael-Scott 无锁链表,基于 CAS 实现节点的插入与删除,是无锁数据结构的基础实现。
    适用场景:高并发系统中的线程间通信、数据缓存、任务分发等场景。

3. RCU(Read-Copy-Update,读 - 复制 - 更新)

核心原理 :读操作完全无锁、无内存屏障,开销几乎为零;写操作时复制数据副本,修改副本后,等待所有正在访问旧数据的读者退出临界区,再原子替换引用并释放旧数据。核心是 "读者零开销,写者延迟释放",是读极多写极少场景的终极无锁方案。
工程实践 :Linux 内核中大规模使用,如网络协议栈、文件系统、设备驱动,解决高频读操作的并发问题;用户态也有对应的 liburcu 库实现。
适用场景:读操作频率远高于写操作(如内核态的系统调用、路由表查询),对读延迟要求极致的场景。

4. 内存屏障(Memory Barrier)

核心原理 :CPU 与编译器会为了性能对指令进行重排序,并发场景下会导致内存可见性问题。内存屏障是 CPU 提供的一组指令,用于禁止指令重排序,保证内存操作的顺序性与可见性,是无锁编程的基础保障。
工程实践

  • x86 架构提供lfence(读屏障)、sfence(写屏障)、mfence(全屏障);

  • Java 的volatile关键字,底层通过内存屏障实现:写 volatile 后插入写屏障,读 volatile 前插入读屏障,保证可见性与禁止重排序;

  • C++ 的std::memory\_order,可精细控制内存序,在无锁编程中平衡性能与安全性。
    适用场景:所有无锁编程场景,是保证并发正确性的底层基础。

四、读写场景专项优化:分离读写冲突,降低竞争概率

针对业务中最常见的 "读多写少" 场景,这类方案通过分离读写操作,让读操作无锁 / 共享,仅写操作排他,大幅降低竞争。

1. 写时复制(COW, Copy-On-Write)

核心原理 :读操作完全无锁无阻塞;写操作时,复制一份数据的完整副本,在副本上完成修改后,通过原子引用替换原数据,整个过程仅写操作之间需要短暂互斥,读写完全不冲突。
工程实践

  • Java 的CopyOnWriteArrayList/CopyOnWriteArraySet,读操作完全无锁,仅写操作加锁复制数组,适合读多写极少的场景;

  • Linux 的fork\(\)系统调用,创建子进程时不复制整个地址空间,父子进程共享内存,仅当子进程修改内存时,才复制对应页面,大幅提升进程创建效率;

  • Redis 的 RDB 持久化、ZFS/Btrfs 文件系统的快照,均基于 COW 实现,避免写操作阻塞读操作。
    适用场景:读多写极少、对数据实时性要求不高(读可能拿到旧版本数据)的场景,如配置管理、白名单、路由表等。

2. 读写锁

核心原理 :将锁拆分为读锁与写锁,读锁是共享锁,多线程可同时持有读锁;写锁是排他锁,写锁与读锁、写锁与写锁之间完全互斥。读多写少场景下,读操作可完全并发,相比排他锁,并发度大幅提升。
工程实践 :Java 的ReentrantReadWriteLock、C++17 的std::shared\_mutex,支持公平 / 非公平模式,可通过写优先级优化避免写饥饿。
适用场景:读多写少、读操作耗时较长的场景,如缓存、元数据管理。

3. 邮戳锁(StampedLock)

核心原理 :JDK8 引入,基于 long 类型的邮戳(stamp)标记锁状态,支持悲观读锁、写锁、乐观读 三种模式。乐观读模式完全无锁,读操作前获取一个邮戳,读完后校验邮戳是否被写操作修改,未修改则读数据有效,整个过程零开销;若校验失败,再升级为悲观读锁重试。
工程实践 :JDK 内置的并发工具中大量使用,性能远超传统读写锁,是读多写少场景的首选优化方案。
适用场景:读极多写极少、对读延迟敏感的场景,如高性能缓存、元数据查询。

4. 多版本并发控制(MVCC)

核心原理 :写操作不覆盖旧数据,而是生成一个新的版本;读操作根据自身的快照版本,读取对应的历史数据,读写完全不互斥,读不会阻塞写,写也不会阻塞读,仅写操作之间需要互斥。
工程实践 :MySQL InnoDB 引擎的核心实现,基于 undo log 保存数据的历史版本,通过 read view 判断版本可见性,实现了读提交、可重复读隔离级别,彻底解决了数据库读写冲突的问题,大幅提升了数据库的并发性能。
适用场景:数据库、分布式存储系统等需要强一致性、高并发读写的场景。

五、选型原则与避坑指南

方案选型优先级(从高到低)

  1. 优先选择无共享架构 / 数据隔离,从根源消除竞争,成本低、收益高;

  2. 其次选择读写分离方案(COW、MVCC、StampedLock),适配读多写少的主流业务场景;

  3. 再次选择无锁编程(原子类、无锁数据结构),适配高并发短操作的底层场景;

  4. 最后选择锁优化,仅当上述方案无法满足需求时,再通过优化锁降低竞争。

无锁编程核心避坑点

  1. ABA 问题:必须通过版本号 / 邮戳机制解决,避免业务逻辑误判;

  2. 重试风暴:大量线程同时 CAS 失败会导致 CPU 飙升,需通过分段隔离、随机退避策略缓解;

  3. 内存序问题:无锁编程必须正确设置内存屏障,避免指令重排序导致的并发 bug;

  4. 内存回收问题:无锁数据结构的节点删除,需通过 RCU、 hazard 指针等机制,确保无线程访问后再释放,避免野指针。

相关推荐
wangguanghou17 小时前
稳定性负责人的职责
软件工程
互联网推荐官1 天前
上海APP开发技术路径深度解析:从架构选型到工程落地
人工智能·架构·软件工程
极创信息2 天前
信创产品认证怎么做?信创产品测试认证的主要流程
java·大数据·数据库·金融·软件工程
早日退休!!!2 天前
自动微分、数值微分、符号微分对比总结
软件工程
张较瘦_4 天前
[论文阅读] AI + 软件工程 | 突破LLM代码生成瓶颈:编程知识图谱(PKG)让检索增强更精准
论文阅读·人工智能·软件工程
肖有米XTKF86464 天前
河北奢源水光商城系统制度开发
人工智能·软件工程·团队开发·csdn开发云
肖有米XTKF86464 天前
二二复制裂变小程序系统制度(双轨制公排模式)
人工智能·小程序·软件工程·团队开发
思茂信息5 天前
CST软件如何进行参数化扫描?
运维·开发语言·javascript·windows·ecmascript·软件工程·软件需求
互联网推荐官5 天前
上海物联网应用开发技术路径拆解:从协议选型到平台架构的工程实践
大数据·人工智能·软件工程