参考:
什么是CAS和ABA问题?如何解决? - 知乎 面试官:你了解乐观锁和悲观锁吗?CAS 是如何实现的?
目录
一、悲观锁
- 悲观锁顾名思义就是:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。Java 中的
synchronized
和ReentrantLock
是悲观锁的典型实现方式。- 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
- 悲观锁通常用于并发环境下的数据库系统,是数据库本身实现锁机制的一种方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。
二、乐观锁
- 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。
- Java 中的
AtomicInteger
和LongAdder
等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。在数据库中,乐观锁并不会使用数据库提供的锁机制,一般在表中添加 version 字段或者使用业务状态来实现。- 乐观锁直到提交时才锁定,所以不会产生任何死锁。
- 乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。
三、怎么实现乐观锁
(一)版本号机制
- 版本号机制是通过比较版本号确保数据一致性。
具体实现:
一般是在数据表中加上一个数据版本号
version
字段,表示数据被修改的次数。当数据被修改时,version
值会+1。当线程 A 要更新数据值时,在读取数据的同时也会读取version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version
值相等时才更新,否则重试更新操作,直到更新成功。例子:
假设数据库中帐户信息表counters中有一个 version 字段,设当前值为 1 、当前帐户余额字段(cash )为 $1000 。
操作员 A 此时将其读出
version
=1 ,并从其帐户余额中扣除($1000-$500=$500 )。在操作员 A 操作的过程中,操作员 B 也读入此用户信息
version
=1 ,并从其帐户余额中扣除 ($1000-$200=$800 )。操作员 A 先完成了修改工作,将数据版本号(
version
=1 ),连同帐户扣除后余额$500 ,提交至数据库并更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录version
的值+1,更新为 2 。操作员 B 也完成了操作,也将版本号(
version
=1 )试图向数据库提交数据余额$800 但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
(二)CAS 算法
- CAS 的全称是 Compare And Swap(比较与交换),被广泛应用于各大框架中。
- CAS 的思想是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
- CAS 是一个原子操作(即最小不可拆分的操作,也就是操作一旦开始,就不能被打断,直到操作完成),底层依赖于一条 CPU 的原子指令。
CAS 涉及到三个操作数:
V:要更新的变量值(Var)
E:预期值(Expected)
N:拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
例子:
线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6)。
i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
ABA问题:
上面的例子还会出现一种问题,就是ABA问题
比如 :因为程序的问题,启动了两个线程1,2
- 线程1 取款$100:从数据库余额表中 获取余额(要更新的值V= 200 元),与预期值E= 200 元比对成功,取出 $100 ,更新余额为100,所以拟写入的新值N=100
- 线程2 取款$100:从数据库余额表中要更新的值V= 200 元,进程阻塞等待修改。
- 线程3 接收转账¥100:从数据库余额表中要更新的值V= 100 元 ,与预期值E= 100 比对成功,接收转账 ¥100 ,更新余额为200 ,所以拟写入的新值N=200
- 线程2:取款$100:恢复执行,要更新的值V= 200 元,与预期值E= 200 元对比成功,取出 $100 ,更新余额为100,所以拟写入的新值N=100
- 最终的结果是 余额为100 元。但实际应该是200元,(200-100+100=200)
ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了,这就是 ABA 问题。
解决 ABA 问题的一种方法是使用带版本号的 CAS,也称为双重 CAS(Double CAS)或者版本号 CAS。
因此,每次进行 CAS 操作时,不仅需要比较要更新的变量值与期望的值是否相等,还需要比对数据库版本号是否相等。如果相等,才进行修改操作。所以不仅要读取要更新的变量值,还需要读取数据版本号
version
。
这样即使变量的值从 A 变成了 B 再变成了 A,版本号也会发生变化,从而避免了误判。