乐观锁与悲观锁

151 阅读2分钟

概念

乐观锁: 乐观锁总是假设最好的情况,认为线程每次访问共享资源的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只在提交修改的时候去验证对应的资源是否被其它线程修改了。

悲观锁: 悲观锁总是假设最坏的情况,认为线程每次访问共享资源的时候都会出现问题,所以每次获取资源操作的时候都会上锁,其他线程要想拿到这个资源就需要要等待,直到锁被上一个持有线程释放。

如何实现乐观锁?

乐观锁一般会采用版本号机制或者 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 非常 大的开销。