概念
乐观锁: 乐观锁总是假设最好的情况,认为线程每次访问共享资源的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只在提交修改的时候去验证对应的资源是否被其它线程修改了。
悲观锁: 悲观锁总是假设最坏的情况,认为线程每次访问共享资源的时候都会出现问题,所以每次获取资源操作的时候都会上锁,其他线程要想拿到这个资源就需要要等待,直到锁被上一个持有线程释放。
如何实现乐观锁?
乐观锁一般会采用版本号机制或者 CAS 算法实现。
版本号机制:
一般是在数据库添加一个 version 字段,数据每次给修改后就+1。比如线程 A 要更新数据时,首先将数据先读取下来,此时 version 的值为 1,在提交 SQL 更新的时候,将刚才读取到的 version 的值与数据库中的 version 的值对比,相同时才更新。
update tb_user_wallet set money = #{money} where version = #{version} and user_id = #{userId};
CAS算法:
CAS 全称 Compare And Swap(比较与交换),主要的思想就是讲一个当前值 E 和要更新的值 V 进行比较,它们两个相等才会更新。
- V:主存中的值(要更新的值)
- E:本地副本值(期望值)
- N:要写入的新值
乐观锁存在哪些问题?
1.ABA问题
ABA 问题是这么一种情况,线程 A 和线程 B 同时更新 V 的值,并且:
线程 A,V = 1,E = 1,N = 2
线程 B,V = 1,E = 1,N = 2
但是线程 A 先获得 CPU 的时间片,线程 A 经过比较发现相等,将主存中 V 的值更新为 2,线程 B 因为某些原因阻塞了,此时来了个线程 C,线程 C 读取主存中 V 的值为 2,获得时间片并比较相等,将主存中 V 的值又更新为1,此时线程 B从阻塞中恢复过来并获得时间片,这时候线程 B读取主存中 V 的值为 1,和期望值相等,也进行了更新,虽然线程 B 也完成了更新操作,但是它对线程 C 的操作是未知的。
ABA 解决思路: 添加一个版本标识、时间戳或者布尔类型,在 jdk1.5 之后可以使用 AtomicStampedReference 类。
2.循环时间开销大
CAS 在更新失败后,会通过自旋操作来实现重试,一直自旋到成功为止,那么长时间不成功就会造成 CPU 非常 大的开销。