栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

CAS:并发同步性不一定要靠互斥锁

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

CAS:并发同步性不一定要靠互斥锁

CAS:并发同步性不一定要靠互斥锁
    • 引言
    • CAS的实现原理
    • Java中CAS的实现
    • 原子类概览

引言

在一个最简单的变量累加操作上,如果引入多线程执行,必然带来可见行与同步性的问题,那么通常来讲,利用volatile可以解决线程间变量的可见行,互斥锁可以实现线程间变量的同步问题。

public class Test {
  long count = 0;
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
}

其实对于上述这种多线程下变量值并发修改的问题,可以有一种无锁的解决方案,那就是今天的主角:CAS!

public class Test {
  AtomicLong count = new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的 count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使add10K() 方法变成线程安全的,原子类的使用还是挺简单的。

无锁方案相对互斥锁方案,最大的好处就是性能。

  • 互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;
  • 同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。
CAS的实现原理

原子类性能高的秘密很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。

你可以通过下面代码理解CAS的原理:只有当目前 count,的值和期望值 expect 相等时,才会将 count 更新为 newValue。

class SimulatedCAS{
   int count;
   synchronized int cas(int expect, int newValue){
     // 读目前 count 的值
     int curValue = count;
     // 比较目前 count 值是否 == 期望值
     if(curValue == expect){
       // 如果是,则更新 count 的值
       count = newValue;
     }
     // 返回写入前的值
     return curValue;
   }
}

通过CAS的语义是不是和上面的测试例子add方法是一样的,只有当前count值是符合期望的,也就是和上一次修改前比没有被别的线程修改,才会执行count+1。这样,上面的情况就可以用CAS这种无锁方案来解决了。

使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。例
如,实现一个线程安全的count += 1操作,“CAS+ 自旋”的实现方案如下所示,首先计
算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着
线程在执行完代码①处之后,执行代码②处之前,count 的值被其他线程更新过。那此时
该怎么处理呢?可以采用自旋方案,就像下面代码中展示的,可以重新读 count 最新的值
来计算 newValue 并尝试再次更新,直到成功。

class SimulatedCAS{
  volatile int count;
  // 实现 count+=1
  addOne(){
    do {
     newValue = count+1; //①
    }while(count != cas(count,newValue) //②
  }
  // 模拟实现 CAS,仅用来帮助理解
  synchronized int cas(int expect, int newValue){
    // 读目前 count 的值
    int curValue = count;
    // 比较目前 count 值是否 == 期望值
    if(curValue == expect){
      // 如果是,则更新 count 的值
      count= newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

CAS 这种无锁方案,完全没有加锁、解锁操作,即便两个线程完全同时执行 addOne() 方法,也不会有线程被阻塞,所以相对于互斥锁方案来说,性能好了很多。

但是在 CAS 方案中,有一个问题可能会常被你忽略,那就是ABA的问题。

上面例子中如果cas(count,newValue) 返回的值等于count,是否就能够认为 count 的值没有被其他线程更新过呢?显然不是的,假设 count 原本是 A,线程 T1 在执行完代码①处之后,执行代码②处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了,这就是 ABA 问题。

可能大多数情况下我们并不关心 ABA 问题,例如数值的原子递增,但在例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A 的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先 check 一下。

Java中CAS的实现

原子类 AtomicLong 的 getAndIncrement() 方法内部就是基于 CAS 实现的,下面我们来看看 Java 是如何使用 CAS 来实现原子化的count+= 1的。

在 Java 1.8 版本中,getAndIncrement() 方法会转调 unsafe.getAndAddLong() 方法。这里 this 和 valueOffset 两个参数可以唯一确定共享变量的内存地址。

final long getAndIncrement() {
  return unsafe.getAndAddLong(this, valueOffset, 1L);
}

public final long getAndAddLong(Object o, long offset, long delta){
  long v;
  do {
    // 读取内存中的值
    v = getLongVolatile(o, offset);
  } while (!compareAndSwapLong(o, offset, v, v + delta));
  return v;
}
// 原子性地将变量更新为 x
// 条件是内存中的值等于 expected
// 更新成功则返回 true
native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

unsafe.getAndAddLong() 方法的源码如下,该方法首先会在内存中读取共享变量的值,之后循环调用 compareAndSwapLong() 方法来尝试设置共享变量的值,直到成功为止。
compareAndSwapLong() 是一个 native 方法,只有当内存中共享变量的值等于expected 时,才会将共享变量的值更新为 x,并且返回 true;否则返回 fasle。compareAndSwapLong 的语义和 CAS 指令的语义的差别仅仅是返回值不同而已。

getAndAddLong() 方法的实现,基本上就是 CAS 使用的经典范例。所以请你再次体会下面这段抽象后的代码片段,它在很多无锁程序中经常出现。Java提供的原子类里面 CAS 一般被实现为 compareAndSet(),compareAndSet() 的语义和CAS 指令的语义的差别仅仅是返回值不同而已,compareAndSet() 里面如果更新成功,则会返回 true,否则返回 false。

原子类概览


本文不对各类型进行一一讲解,有兴趣的小伙伴可以自行进行针对性学习,本文仅对“引用类型”进行讲解。

原子化的对象引用类型:
相关实现有 AtomicReference、AtomicStampedReference 和AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。AtomicReference 提供的方法和原子化的基本数据类型差不多,这里不再赘述。不过需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和AtomicMarkableReference 这两个原子类可以解决 ABA 问题。

解决 ABA 问题的思路其实很简单,增加一个版本号维度就可以了,每次执行 CAS操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回A,版本号也不会变回来(版本号递增的)。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数。

boolean compareAndSet(V expectedReference,V newReference,
  int expectedStamp,int newStamp)

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:

boolean compareAndSet(V expectedReference,V newReference,
  boolean expectedMark,boolean newMark)

总结:
觉得有用的客官可以点赞、关注下!感谢支持谢谢!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/490625.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号