-
悲观锁
-
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
-
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性
-
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及 syncronized 实现的锁均为悲观锁
-
-
乐观锁
-
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。
-
如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)
-
乐观锁适用于多读的应用类型,这样可以提高吞吐量
-
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。
-
一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
-
乐观控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
-
-
锁的实现
-
悲观锁阻塞事务、乐观锁回滚重试
-
乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量
-
如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
-
悲观锁的实现
-
悲观锁的实现,往往依靠数据库提供的锁机制
-
悲观锁加锁 sql 语句:
select ... where ... for update
-
-
乐观锁的实现
-
使用版本号(version)来解决,对比前后的version的异同来判断是否是自己事务开启的时候那个值
-
CAS
-
Compare-and-Swap,即比较并替换,也有叫做 Compare-and-Set 的,比较并设置。
-
具体的过程
-
1、比较:读取到了一个值 A,在将其更新为 B 之前,检查原值是否仍为 A(未被其他线程改动)。
-
2、设置:如果是,将 A 更新为 B,结束。如果不是,则什么都不做。
-
两步操作是原子性的,可以简单地理解为瞬间完成,在 CPU 看来就是一步操作
-
-
有了 CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS 利用 CPU 指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
-
Java 中真正的 CAS 操作调用的 native 方法因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。
-
乐观锁其实不是“锁”,它仅仅是一个循环重试 CAS 的算法而已
-
CAS 有一个问题那就是会产生 ABA 问题,什么是 ABA 问题,以及如何解决呢?
-
ABA 问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
-
ABA 问题解决:我们需要加上一个版本号(Version),在每次提交的时候将版本号+1 操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息
-
-
-
-
如何选择
-
悲观锁阻塞事务,乐观锁回滚重试
-
乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量
-
如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适
-
注意点
-
乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
-
悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
-
-
-
乐观锁和悲观锁的实现
最新推荐文章于 2024-08-21 23:28:12 发布