一、核心思想:不同的并发哲学
乐观锁和悲观锁是处理数据竞争(多个线程可能同时修改同一数据)的两种不同策略。它们的区别源于对“冲突发生概率”的不同假设。
二、对比总结先行
特性悲观锁乐观锁哲学假设冲突很可能发生假设冲突不太可能发生机制先取锁,再操作先操作,更新前再检查冲突实现synchronized, ReentrantLock, FOR UPDATECAS、版本号、原子变量线程状态其他线程会**被阻塞(挂起) **其他线程不被阻塞,可继续执行,但可能需要重试开销大(加锁、解锁、上下文切换)小(无锁操作,但冲突时重试有开销)适用场景写操作多,冲突频繁的场景读操作多,冲突稀少的场景三、悲观锁
1. 定义
悲观锁假定冲突非常可能发生。因此,它在访问共享数据之前,会先独占性地锁定资源,阻止其他任何线程访问,直到它完成操作并释放锁。
2. 工作流程
- 获取锁:线程A在读取数据前,先成功获取到资源的锁。
- 阻塞他人:线程B也想访问该资源,但发现锁已被A持有,于是线程B被挂起(进入阻塞状态),等待锁被释放。
- 操作数据:线程A可以安心地读取数据、进行计算、修改数据。这个过程是排他的。
- 释放锁:线程A完成操作,释放锁。
- 唤醒他人:锁被释放后,系统唤醒正在等待的线程B(和其他线程),它们可以尝试重新获取锁以访问资源。
3. 实现方式
- synchronized 关键字(Java)
- ReentrantLock 等显式锁(Java)
- SELECT ... FOR UPDATE(数据库中的行锁/表锁)
4. 优缺点
- 优点:简单粗暴,能保证最高的数据安全性和一致性。
- 缺点:性能开销大。加锁和释放锁的操作本身消耗资源,更重要的是,线程的挂起和唤醒是非常昂贵的操作,会导致上下文切换。如果一个线程持有锁的时间很长,其他所有线程都会被阻塞,严重影响系统吞吐量。
5.适用场景
写多读少的场景,即冲突发生的概率确实很高。在这种情况下,乐观锁会频繁失败重试,反而可能比悲观锁的直接阻塞性能更差。
四、乐观锁
1. 定义
乐观锁假定冲突不太可能发生。因此,它不会在访问数据时立即加锁,而是先直接操作数据。但在更新数据的那一刻,它会检查在此期间是否有其他线程修改过这个数据。如果没有,就更新成功;如果有,就更新失败,并进行相应的处理(通常是重试或报错)。
2. 工作流程
- 读取与记录:线程A读取数据,并记录下数据的版本号(Version)或某种校验值(如CAS中的旧值)。
- 操作数据:线程A在本地进行计算修改(这一步不加锁,所以其他线程可能已经修改了数据)。
- 写前验证:线程A准备将更新写回时,会检查当前的版本号是否和它最初读取的版本号一致。
- 如果一致:说明没有其他线程修改过数据,于是执行更新操作,并通常会增加版本号。
- 如果不一致:说明数据已被其他线程修改,本次更新失败。
- 失败处理:更新失败后,常见的策略是重试(重新读取最新数据和版本号,然后再次执行整个计算和更新流程)。
3. 实现方式
- CAS指令:CPU级别的原子操作(Compare-And-Swap)。它是乐观锁技术的底层基石。
- 操作:CompareAndSet(oldValue, newValue)。只有在当前值等于 oldValue 时,才会将其设置为 newValue。
- 原子类:Java中的 AtomicInteger、AtomicLong、AtomicReference 等就是基于CAS实现的。
- 版本号机制:数据库和乐观锁框架(如Hibernate)中常用。在数据表中增加一个 version 字段。
4. 优缺点
- 优点:性能高。在没有冲突的情况下,它完全避免了加锁、解锁、线程阻塞和上下文切换的开销,吞吐量极高。
- 缺点:
- ABA问题:一个值原来是A,被另一个线程改为B,后又改回A。使用CAS的线程会误以为它没变过。通常通过附加版本号或时间戳来解决。
- 循环时间长开销大:如果冲突频繁,重试操作会持续进行,可能CPU开销很大。
*** 只能保证一个共享变量的原子操作**。对多个共享变量操作时,CAS无法保证原子性,可能需要用锁。
5.适用场景
读多写少的场景,即冲突发生的概率很低。这是它的理想舞台,能发挥其无锁化的巨大性能优势。
五、结论
选择哪种锁,取决于你对冲突概率的判断。在高并发的互联网应用中,读远多于写是非常普遍的情况,因此乐观锁(及其变种)的应用更为广泛。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |