Java基础、中级、高级、架构面试资料

从“图”学习乐观锁与悲观锁(CAS以及ABA问题)

JAVA herman 3466浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

前面写了好几篇关于 HashMap 的文章,有人在微信群里给我反馈说想看点别的。其实我也想换了,HashMap 写来写去就那么点内容,还能写出 bug 出来(大笑)!所以,索性我今天就换个新内容!乐观锁 VS 悲观锁!

其实吧,写锁也写不出来什么新花样,于是我就在考究如何和别人不一样呢?答案就是我今天的标题,从“图”理解乐观锁与悲观锁(Java 中所有很多,我今天只聊乐观锁与悲观锁这两个简单的,其他的锁我后面再说!可以参考我的这篇《学会 Java 中的锁,你只需要记住 6 句法则即可!》文章)!

乐观锁与悲观锁是一种广义上的概念。不管是 Java 语言,也或者是其他语言以及数据库都有这类概念对应的实际应用。

悲观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁 VS 悲观锁

根据上面的文章描述+图片说明,我相信你已经完全理解了乐观锁和悲观锁。

概念我理解啊,但是实际应用 ……

好吧,看下面的代码:

// 悲观锁的调用方式
// synchronized
public synchronized void testMethod() {
    // 操作同步资源
}
// ReentrantLock 需要保证多个线程使用的是同一个锁
private ReentrantLock lock = new ReentrantLock(); 
// 
public void modifyPublicResources() {
    lock.lock();
    // 操作同步资源
    lock.unlock();
}
// 业余草:www.xttblog.com
// 乐观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger();  
// 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

现在应该明白了哪些是悲观锁哪些是乐观锁了吧。没理解的话,就先死记硬背它!

从上面的代码可以看出,乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 中一些原子类(如:AtomicInteger)的递增操作就通过 CAS 自旋实现的。这也就是为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步的!

CAS

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁。

CAS 算法涉及到三个核心操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

前面提到的原子类,比如 AtomicInteger。我们可以看一下它的源码:

AtomicInteger 关键源码

上面截图中有 3 个地方值得我们去关注:

  • unsafe: 获取并操作内存的数据。
  • valueOffset: 存储 value 在 AtomicInteger 中的偏移量。
  • value: 存储 AtomicInteger 的 int 值,该属性需要借助 volatile 关键字保证其在线程间是可见的。

在使用 AtomicInteger 时,我们用的最多的是 incrementAndGet 方法,下面我们看看它的源码:

// JDK 8
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class 业余草:www.xttblog.com
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

整个“比较+更新”操作封装在 compareAndSwapInt() 中,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

CAS 虽然很高效,但是它也存在三大问题,这里也简单说一下:

  • ABA 问题
  • 循环时间长开销大
  • 只能保证一个共享变量的原子操作

ABA 问题

CAS 需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了 B,然后又变成了 A,那么 CAS 进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,具体操作封装在 compareAndSet()中。compareAndSet() 首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

ABA 问题,我先简单的说到这里,后面抽时间,我们单独来说它!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 从“图”学习乐观锁与悲观锁(CAS以及ABA问题)