减少锁竞争与无锁化
锁竞争的核心代价,来自临界区串行化导致的吞吐量下降、线程阻塞 / 唤醒的内核态切换、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 的ReentrantLock、synchronized,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包:AtomicInteger、AtomicLong、AtomicReference、带版本号的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 判断版本可见性,实现了读提交、可重复读隔离级别,彻底解决了数据库读写冲突的问题,大幅提升了数据库的并发性能。
适用场景:数据库、分布式存储系统等需要强一致性、高并发读写的场景。
五、选型原则与避坑指南
方案选型优先级(从高到低)
-
优先选择无共享架构 / 数据隔离,从根源消除竞争,成本低、收益高;
-
其次选择读写分离方案(COW、MVCC、StampedLock),适配读多写少的主流业务场景;
-
再次选择无锁编程(原子类、无锁数据结构),适配高并发短操作的底层场景;
-
最后选择锁优化,仅当上述方案无法满足需求时,再通过优化锁降低竞争。
无锁编程核心避坑点
-
ABA 问题:必须通过版本号 / 邮戳机制解决,避免业务逻辑误判;
-
重试风暴:大量线程同时 CAS 失败会导致 CPU 飙升,需通过分段隔离、随机退避策略缓解;
-
内存序问题:无锁编程必须正确设置内存屏障,避免指令重排序导致的并发 bug;
-
内存回收问题:无锁数据结构的节点删除,需通过 RCU、 hazard 指针等机制,确保无线程访问后再释放,避免野指针。