多线程编程中的锁策略

目录

1.悲观锁vs乐观锁

关键总结

悲观锁:

乐观锁:

选择建议

[用 悲观锁 当:](#用 悲观锁 当:)

[用 乐观锁 当:](#用 乐观锁 当:)

2.重量级锁vs轻量级锁

选择建议

[用 轻量级锁:](#用 轻量级锁:)

[用 重量级锁:](#用 重量级锁:)

3.挂起等待锁vs自旋锁

关键细节说明

选择建议

4.普通互斥锁vs读写锁

读写锁互斥规则详解

[普通互斥锁 vs 读写锁对比](#普通互斥锁 vs 读写锁对比)

5.可重入锁vs不可重入锁

选择建议

6.公平锁vs不公平锁

[1) 公平锁和不公平锁的对比](#1) 公平锁和不公平锁的对比)

2)选择建议

7.锁升级

8.锁优化

9.锁的粗化

10.synchronized

11.reentrantlock和synchronized的对比


锁策略(Locking Strategies)是多线程编程中用于控制并发访问共享资源的核心机制。不同的锁策略在性能、公平性、复杂度等方面有显著差异。

以下来介绍几组常见的锁策略:

1.悲观锁vs乐观锁

悲观锁 :在加锁的时候预测接下来的竞争会非常激烈 ,所以就需要针对这种激烈的竞争情况做一些准备工作:每次拿数据的时候都会上锁 ,这样别人 想要拿数据的时候就会阻塞知道它拿到锁。

乐观锁 :在加锁的时候预测接下来的竞争不会非常激烈,也就一般不会发生并发冲突,不必多做准备。在数据进行提交更新的时候再检测是否产生并发冲突。

特性 悲观锁 乐观锁
核心思想 假设并发冲突一定会发生 ,因此先加锁再访问数据。 假设并发冲突较少发生 ,因此不加锁,通过版本号/CAS机制检测冲突。
实现方式 直接加锁(如 synchronizedReentrantLock、数据库 SELECT FOR UPDATE)。 无锁机制(如 CAS、版本号校验、AtomicXXX 类、数据库 乐观并发控制)。
阻塞行为 会阻塞其他线程(未获取锁的线程必须等待)。 无阻塞,线程直接操作数据,冲突时通过重试或回滚解决。
适用场景 写多读少(如银行转账、库存扣减等强一致性场景)。 读多写少(如缓存、统计计数等低冲突场景)。
性能开销 高(锁竞争、上下文切换、死锁风险)。 低(无锁竞争,但冲突频繁时重试开销增大)。
数据一致性 强一致性(锁内操作串行化)。 最终一致性(冲突时需业务层处理)。
典型实现 Java: synchronizedReentrantLock 数据库: SELECT FOR UPDATE Java: AtomicIntegerStampedLock 数据库: 版本号字段CAS操作
优缺点 ✅ 保证强一致性 ❌ 吞吐量低,可能死锁。 ✅ 高并发性能 ❌ 需处理冲突,可能活锁或重试饥饿。

关键总结

悲观锁

  • "先保护再操作",适合强一致性场景,但性能较差。

  • 例如:synchronized 关键字、数据库行锁。

乐观锁

  • "先操作再校验",适合高并发读场景,冲突少时性能极高。

  • 例如:CAS 操作、StampedLock 的乐观读模式。


选择建议

悲观锁 当:

  • 写操作频繁,或冲突概率高(如支付系统)。

  • 业务逻辑复杂,需严格保证数据一致性。

乐观锁 当:

  • 读多写少,且冲突概率低(如商品浏览统计)。

  • 追求高吞吐量,能容忍重试或短暂不一致。

2.重量级锁vs轻量级锁

重量级锁:应对悲观的场景,此时就要付出更多的代价,更低效。

轻量级锁:应对乐观的场景,此时付出的代价更小,更高效。

特性 重量级锁 轻量级锁
锁级别 最高级别的锁(如操作系统级互斥锁) JVM 层面的优化锁(基于 CAS 自旋)
实现原理 通过操作系统内核的 互斥量(Mutex) 实现,涉及用户态/内核态切换。 通过 CAS(Compare-And-Swap)线程栈中的锁记录(Lock Record) 实现。
阻塞方式 未获取锁的线程会 直接挂起(进入内核态等待队列),由操作系统调度唤醒。 先通过 自旋(忙等待) 尝试获取锁,失败后才升级为重量级锁。
性能开销 高(上下文切换、内核态切换、CPU 调度延迟)。 低(无系统调用,仅在用户态自旋)。
适用场景 高竞争、长临界区(如长时间持有锁的复杂操作)。 低竞争、短临界区(如快速计算的简单操作)。
锁升级 是锁膨胀的最终状态(轻量级锁/偏向锁失败后升级)。 是偏向锁失败后的中间状态,可能进一步升级为重量级锁。
典型例子 synchronized 在竞争激烈时的最终状态。 synchronized 在低竞争时的优化状态。
优点 严格保证线程安全,适合高并发冲突场景。 减少内核态开销,提高短任务性能。
缺点 吞吐量低,响应延迟高。 自旋浪费 CPU(长时间自旋可能反而降低性能)。

选择建议

轻量级锁

  • 代码块执行时间极短(如计数器递增)。

  • 线程竞争概率低(如局部变量同步)。

重量级锁

  • 临界区执行时间长(如数据库事务)。

  • 竞争激烈(如全局缓存更新)。

3.挂起等待锁vs自旋锁

挂起等待锁 :(重量级锁的经典实现)一旦发生竞争,获取不到锁,就进入阻塞状态。

自旋锁 :(轻量级锁的经典实现)一旦发生竞争,获取不到锁,就循环请求,也就是"忙等 "。发生在"乐观 "情况下,锁竞争很小,即使"忙等",很快也能获取锁,等待的时间也不会很长,消耗的资源也就不会很多。

特性 挂起等待锁(阻塞锁) 自旋锁
实现原理 获取不到锁时,线程进入阻塞状态(WAITING),释放CPU资源 获取不到锁时,线程循环忙等待(自旋),不释放CPU
响应速度 较慢(需要线程切换和唤醒) 极快(无上下文切换)
CPU占用 低(线程挂起后不消耗CPU) 高(持续占用CPU自旋)
适用场景 锁持有时间较长(如I/O操作、复杂计算) 锁持有时间极短(如CAS操作、简单计算)
公平性 通常支持公平锁(按申请顺序获取锁) 通常是非公平锁(竞争机制)
锁升级 可能涉及内核态切换(如synchronized重量级锁) 完全在用户态运行(无系统调用)
典型实现 Java的synchronized(竞争激烈时)、ReentrantLock Java的AtomicIntegerSpinLock(自定义实现)
优点 节省CPU资源,适合高竞争场景 无上下文切换,延迟极低
缺点 线程切换开销大,响应延迟高 自旋过久会浪费CPU,可能饥饿
死锁风险 可能死锁(需超时或中断机制) 无死锁(但可能活锁)

关键细节说明

  1. 挂起等待锁(阻塞锁)

    • 适用场景:锁竞争激烈或临界区执行时间较长(如数据库事务)。

    • 优化建议

      • 使用ReentrantLock替代synchronized(支持超时和中断)。

      • 设置合理的锁超时时间(避免死锁)。

  2. 自旋锁

    • 适用场景:锁持有时间极短(如计数器递增)。

    • 优化建议

      • 限制自旋次数(如JVM的-XX:PreBlockSpin参数)。

      • 升级为自适应自旋锁(根据历史自旋时间动态调整)。


选择建议

  • 优先用自旋锁

    • 临界区代码执行时间 < CPU上下文切换时间(通常约1μs)。

    • 例如:AtomicInteger的CAS操作。

  • 优先用挂起等待锁

    • 临界区代码执行时间 > 自旋消耗的CPU时间

    • 例如:同步访问数据库连接池。

4.普通互斥锁vs读写锁

普通互斥锁 :锁之间完全互斥,同一时间只能有一个线程持有锁,分为'加锁'和'解锁'两个过程。synchronized就是经典的普通互斥锁。

读写锁 :应对某些读多写少 的情况,分为**"读锁** "和"写锁 "两种,其中**"读锁"和"读锁"之间不互斥**,支持了多个线程同时读取的情况,减少了锁冲突的情况。

读写锁互斥规则详解

  1. 读锁 vs 读锁

    • 不互斥:多个线程可同时持有读锁(共享)。

    • 示例:100个线程可同时读取缓存数据。

  2. 读锁 vs 写锁

    • 完全互斥

      • 已有读锁时,写锁请求被阻塞(直到所有读锁释放)。

      • 已有写锁时,读锁请求被阻塞(直到写锁释放)。

  3. 写锁 vs 写锁

    • 完全互斥:同一时刻只允许一个写锁存在。

普通互斥锁 vs 读写锁对比

特性 普通互斥锁(Mutex Lock) 读写锁(Read-Write Lock)
锁模式 排他锁(Exclusive Lock) 读锁(共享锁) + 写锁(排他锁)
并发性 完全互斥,同一时刻只有一个线程能持有锁 读锁可共享(允许多线程同时读),写锁互斥
适用场景 写操作频繁或读写操作时间相近 读多写少(如缓存、配置读取)
性能 高竞争下性能差 读并发高,写操作会阻塞所有读/写
公平性 支持公平/非公平(如ReentrantLock 通常支持公平/非公平(如ReentrantReadWriteLock
锁降级 不支持 支持(持有写锁的线程可以获取读锁,再释放写锁)
锁升级 不支持 不支持(读锁不能直接升级为写锁,易死锁)
实现复杂度 简单 较复杂(需维护读/写状态)
典型实现 Java: synchronizedReentrantLock Java: ReentrantReadWriteLock
优点 实现简单,严格保证一致性 读操作高并发,适合读多写少场景
缺点 读写均互斥,并发性差 写操作饥饿风险(长期有读锁时写锁无法获取)

5.可重入锁vs不可重入锁

可重入锁:允许同一个线程多次获取一把锁。

不可重入锁:不允许同一个线程多次获取同一把锁。

特性 可重入锁 不可重入锁
递归调用支持 ✅ 同一线程可重复获取同一把锁(计数器递增) ❌ 同一线程重复获取会死锁
死锁风险 低(允许嵌套加锁) 高(递归调用或嵌套加锁直接死锁)
实现复杂度 较高(需维护持有线程和重入次数) 简单(仅检查锁是否被占用)
典型应用场景 递归函数、同步方法调用其他同步方法 简单临界区保护(无嵌套调用场景)
性能开销 略高(需维护重入状态) 较低(无状态跟踪)
锁释放要求 必须释放与获取次数匹配(如加锁3次需解锁3次) 只需释放1次
公平性支持 通常支持(如ReentrantLock(true) 通常不支持
中断响应 支持(lockInterruptibly() 一般不支持
实现示例 Java: synchronizedReentrantLock C++: std::recursive_mutex Java: 自定义简单锁 C: pthread_mutex(默认不可重入)
适用性 通用场景(尤其是复杂同步逻辑) 极简场景(无嵌套/递归)

选择建议

  • 用可重入锁

    • 存在方法嵌套/递归调用同步代码。

    • 使用类库提供的锁(如Java的synchronized)。

  • 用不可重入锁

    • 绝对无嵌套的简单临界区(性能敏感场景)。

    • 需要极轻量级同步原语(如自定义自旋锁)。

6.公平锁vs不公平锁

公平锁:线程采用"先来后到"的思想来依次获取锁。

不公平锁:线程采用"概率相等"的思想来获取锁。

1) 公平锁和不公平锁的对比

特性 公平锁 (Fair Lock) 非公平锁 (Non-Fair Lock)
获取锁顺序 严格按照线程请求顺序(先到先得) 允许插队(新线程可能直接抢到锁)
吞吐量 较低(线程切换频繁) 较高(减少线程切换)
线程饥饿 不会发生(保证每个线程有机会执行) 可能发生(某些线程长期抢不到锁)
实现复杂度 较高(需维护等待队列) 较低(直接竞争)
适用场景 要求严格公平性(如订单处理、金融交易) 高并发场景,追求性能(如缓存、计数器)
锁获取策略 检查是否有等待更久的线程,有则排队 直接尝试获取锁,失败才进入队列
典型实现 ReentrantLock(true) ReentrantLock(false)(默认)、synchronized
响应时间 较稳定(按序执行) 波动较大(可能某些线程更快)
CPU利用率 较低(线程频繁挂起/唤醒) 较高(减少上下文切换)
锁竞争激烈时 所有线程按序执行,但吞吐量下降 可能某些线程一直抢到锁,其他线程等待更久

2)选择建议

场景 推荐锁类型 原因
需要严格顺序(如交易系统) 公平锁 避免某些请求被无限延迟
高并发、低延迟(如缓存) 非公平锁 减少线程切换,提高吞吐量
线程执行时间差异大 公平锁 防止短任务"饿死"长任务
锁竞争不激烈 非公平锁 插队概率低,性能接近公平锁但开销更小

7.锁升级

锁升级是多线程并发控制中的一种优化策略 ,指JVM根据竞争情况动态调整锁的级别,从低开销锁 逐步升级为高开销锁的过程。

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁级别 升级时机 特点 适用场景
无锁 对象未被任何线程锁定 新建对象
偏向锁 代码进入synchronized代码块 =>偏向锁 仅记录线程ID,无实际同步,只是一个标记,不是锁 单线程访问
轻量级锁 其他线程尝试获取这个锁 =>轻量级锁 CAS自旋尝试获取锁 低竞争多线程
重量级锁 JVM发现,当前竞争锁的情况非常激烈 =>重量级锁 操作系统互斥量实现 高竞争场景

总的来说,锁升级是懒汉模式思想的一种体现,旨在减少开销。锁升级只升不降,不可以再转化回去。

8.锁优化

锁优化是提升多线程程序性能的关键手段,JIT编译器通过逃逸分析移除不必要的锁。旨在减少锁的开销。

9.锁的粗化

锁粗化是JVM针对同步代码的一种重要优化技术,它通过合并相邻的同步块减少不必要的锁获取/释放操作,从而提升程序执行效率。

粒度级别 含义
粗粒度 在一把锁之间代码更多
细粒度 在一把锁之间代码更少

锁的粗化实际上就是通过将多把锁合并为一把锁来减少频繁上锁、解锁的开销,使得一把锁之间的代码变得更多。

10.synchronized

锁的类型 synchronized
悲观锁还是乐观锁? 自适应
重量级锁还是轻量级锁? 自适应
挂起等待锁还是自旋锁? 自适应
普通互斥锁还是读写锁? 普通互斥锁
可重入锁还是不可重入锁? 可重入锁
公平锁还是不公平锁? 公平锁

11.reentrantlock和synchronized的对比

ReentrantLock是 Java 并发包(java.util.concurrent.locks)中提供的可重入互斥锁实现,它比传统的 synchronized 关键字提供更灵活的锁操作。

以下是reentrantlocksynchronized的五点区别:

| 区别 | reentrantlock | synchronized |
| 实现方式 | Java标准库中的类,由JDK实现 | 关键字,由JVM实现,JVM基于C++实现 |
| 加锁方法 | 通过lock()和unlock()方法加锁和解锁,有可能因为忘记解锁而触发线程安全问题 | 通过代码块控制加锁、解锁 |
| trylock()方法 | 具备trylock()方法,尝试获取锁而不会无限期阻塞 | 不具备 |
| 公平锁与非公平锁 | 默认是非公平锁,但是也可以实现公平锁 | 非公平锁 |

等待通知机制 等待通知机制是condition类,比synchronized的wait、notify功能更为强大 wait、notify
相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json