悲观锁认为并发冲突常见,因此在操作前加锁以保证独占,如数据库行锁或synchronized;乐观锁假设冲突较少,允许并行操作,在提交时通过版本号或时间戳检查冲突,适用于读多写少场景。两者核心哲学不同:悲观锁追求安全性,牺牲性能;乐观锁追求高并发,容忍重试。选择取决于业务对一致性与性能的权衡。

理解乐观锁和悲观锁,核心在于它们处理并发冲突的哲学截然不同。悲观锁认为冲突是常态,所以它在操作前就直接“锁死”资源,确保独占;而乐观锁则相信冲突不常发生,它允许大家并行操作,只在提交时才检查是否有冲突,如果发现冲突,就让操作失败并重试。
在我看来,理解乐观锁和悲观锁,首先要抓住它们对并发冲突的“态度”。
悲观锁(Pessimistic Locking)
这就像是你在图书馆看书,为了确保没人打扰,你直接把书拿到一个只有你才能进入的私人房间里看。它假定并发冲突是高频事件,所以在数据被修改之前,会先对数据进行加锁,阻止其他事务访问。这种锁通常是数据库层面的,比如行锁、表锁,或者是编程语言层面的互斥锁(如Java的synchronized关键字或ReentrantLock)。
乐观锁(Optimistic Locking) 这更像是大家都在同一个图书馆里看书,每个人都可以拿走一份副本阅读修改,没人会阻止你。只有当你决定把修改后的内容放回原处时,系统才会检查你拿走的那份是不是最新版本。如果发现有人在你修改期间已经更新了原版,那你的修改就会被拒绝,你需要重新获取最新版本再修改。它假定并发冲突是低频事件,不阻塞其他事务对相同数据的读写,而是在更新数据时检测是否发生冲突。
version字段,每次数据更新时,将该字段值加1。更新操作时,会检查当前数据的version是否与你读取时的version一致。-- 伪代码示例:更新库存 UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ?;
如果受影响的行数为0,说明在你更新前,别人已经更新了这条数据,你的操作就需要重试。
我们之所以需要区分这两种锁,说白了,就是因为它们代表了两种截然不同的资源管理和冲突解决策略,而这两种策略在不同的应用场景下,会带来天壤之别的系统表现。它们的核心设计哲学,在我看来,可以归结为:
悲观锁秉持的是一种“防御性编程”的哲学,它预设了最坏的情况——即并发冲突一定会发生,而且会频繁发生。所以,它的策略是“先发制人”,通过独占资源来彻底避免冲突。这就像是过马路,悲观锁会选择在红灯亮起时,彻底停下,等待绿灯亮起,确保绝对安全才通过。这种哲学优先考虑的是数据的一致性和安全性,哪怕牺牲一部分性能。它追求的是操作的确定性,一旦拿到锁,就意味着操作成功有保障。
而乐观锁则是一种“事后诸葛亮”的哲学,它预设了最好的情况——即并发冲突很少发生,或者即便发生,也能通过检测和重试来解决。它的策略是“放手一搏”,让所有操作并行进行,只在最后提交时才验证是否“踩雷”。这就像过马路,乐观锁会选择在人行横道上,相信大多数时候车辆会礼让,只在发现有车冲过来时才紧急避让或重新等待。这种哲学优先考虑的是系统的吞吐量和并发性,它相信大多数时候的并行操作都是无害的。它追求的是高效率,认为为少数可能发生的冲突而牺牲整体性能是不划算的。
选择哪种锁,其实也反映了我们对业务场景并发特性的判断和权衡。没有绝对的好坏,只有是否合适。
在实际开发中,这两种锁的选择,很大程度上取决于你对业务并发特性的判断,以及对性能和数据一致性权衡的优先级。
悲观锁的典型应用场景:
乐观锁的典型应用场景:
AtomicInteger、AtomicLong等原子类就是基于CAS实现的乐观锁思想。乐观锁虽然在提升并发性能上效果显著,但它也并非万能药,实际实现中会遇到一些挑战,需要我们巧妙应对。
ABA问题: 这是乐观锁特有的一个经典问题。假设一个变量V,初始值为A。线程1读取V为A。在线程1准备更新V之前,线程2将V从A修改为B,然后又从B修改回A。此时,线程1再执行更新操作时,会发现V的值仍然是A,认为没有发生过变化,从而成功执行更新。但实际上,V已经被修改了两次。
AtomicStampedReference就是为了解决ABA问题而设计的,它不仅比较值,还会比较一个“标记”(stamp),每次修改都增加标记值。这样,即使值变回去了,标记值也会不同,从而识别出中间的修改。// 伪代码:使用AtomicStampedReference解决ABA问题 // AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 0); // int currentStamp = asr.getStamp(); // boolean success = asr.compareAndSet(100, 120, currentStamp, currentStamp + 1); // currentStamp = asr.getStamp(); // 获取新的stamp // boolean success2 = asr.compareAndSet(120, 100, currentStamp, currentStamp + 1); // 即使值变回100,stamp也变了 // currentStamp = asr.getStamp(); // boolean finalSuccess = asr.compareAndSet(100, 90, oldStampFromThread1, oldStampFromThread1 + 1); // 线程1会失败
活锁(Livelock): 当多个线程都尝试进行乐观更新,但由于冲突频繁,它们不断地重试,却又不断地失败,导致所有线程都处于忙碌状态,但没有一个能成功完成操作。这就像两个人在窄路上相遇,都想让路,结果你往左我往左,你往右我往右,谁也过不去。
高并发冲突率下的性能下降: 乐观锁的优势在于冲突率低时性能高。但如果实际业务场景中冲突非常频繁,那么大量的重试操作反而会消耗大量的CPU资源和网络带宽,导致整体性能不升反降,甚至比悲观锁更差。
业务逻辑复杂性增加: 乐观锁的实现通常需要应用层代码来处理版本号的比较、冲突的检测以及失败后的重试逻辑。这相比悲观锁的“加锁-操作-解锁”模式,无疑增加了开发和维护的复杂性。
总的来说,乐观锁不是银弹,它要求我们对业务场景有更深刻的理解和更精细的设计。
以上就是理解乐观锁和悲观锁的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号