前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CAS——比加锁更高效的多线程并发场景下数据一致性解决方案

CAS——比加锁更高效的多线程并发场景下数据一致性解决方案

作者头像
cheese
发布2024-06-15 10:55:10
420
发布2024-06-15 10:55:10
举报
文章被收录于专栏:Java PorterJava Porter

前置基础

  • 原子类java.util.concurrent.atomic
  • 该类下所有的包均源自 CAS 思想的落地实现

CAS 出现前后

  • case demo,多线程环境下原子类如何保证线程安全 i++(基本数据类型)

CAS 出现以前(不使用原子类)

代码语言:javascript
复制
public class CaseDemo {
    //不使用原子类(CAS)
    volatile int number = 0;
    //读数据
    public int getNumber(){
        return number;
    }
    //写数据,加锁,保证数据原子性
    public synchronized void setNumber(){
        number++;
    }
}

CAS 出现之后(使用原子类)

代码语言:javascript
复制
    //CAS出现以后
    AtomicInteger atomicInteger = new AtomicInteger();
    public int getAtomicInteger(){
        return atomicInteger.get();
    }
    public void setAtomicInteger(){
        //获取并执行i++
        atomicInteger.getAndIncrement();
    }

两种方式的比较

  • 使用原子类的代码更加简洁和方便
  • 从性能角度上,对值的修改,原子类无需加重量级锁,更加类似于乐观锁的实现方式

CAS 是什么

简介

  • compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
  • 它包含三个操作数–内存位置、预期原值及更新值。
  • 执行CAS操作的时候,将内存位置的值与预期原值比较:
    • 如果相匹配,那么处理器会自动将该位置值更新为新值
    • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

实现原理

  • 图示的三个操作数,位置内存值 V,旧的预期值 A,待更新值 B
  • 当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则不做任何操作或是重来
  • 当它重来重试的这种行为称为自旋
  • example
  • 每个线程都基于乐观锁思想进行执行
  • 存在以下一种情况
    • 线程 A 读取 5 到本地时,乐观的认为没有其它线程并发执行
    • 线程 A 执行完 a++后将 a 写回内存时,发现 a 已经被线程 B/C 执行过 a++,
      • 存在一种情况,当 线程 A 将 a 写回内存时,此时内存中的 A 变为 7
    • 则内存中 a 大于线程 A 将要写回内存的 6,则线程 A 重新执行一次自旋
    • 线程 A 自旋之后若在此期间 内存中的 A 又发生更改则 A 需要再次自旋,若没有线程改变内存中的 a,则线程 A 自旋后完成本次修改
  • 小结,CAS 类似于乐观锁的版本机制

CaseDemo

代码语言:javascript
复制
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        System.out.println(atomicInteger.compareAndSet(5, 2024) + "\t" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2024) + "\t" + atomicInteger.get());

        atomicInteger.getAndIncrement();
    }
  • running result
image.png
image.png
  • 初始化内存数据是 5
    • 首次获取期望值是 5,与内存数据一致,满足可修改条件,修改为 2024
    • 第二次期望值是 5,但是内存数据已经被修改为 2024,不满足数据可更新条件

硬件级别对一致性的保证

  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
  • 它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
  • CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

对源码的理解

  • compareAndSet()方法的源代码
代码语言:javascript
复制
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

以上三个方法均可类比,主要对 4 个参数做简要说明

  • var1:表示要操作的对象
  • var2:表示要操作对象属性地址的偏移量
  • var4:表示需要修改的数据的期望值
  • var5/6:表示需要修改为的新值

CAS 底层原理与 Unsafe

Unsafe 类

  • AtomicInteger 源码视图
  • 原子类 AtomicInteger 原子特性的得益于 UnSafe 类的使用,
  • UnSafe 类是基于 CAS 原理实现的一个 JDK 自带的能够操作操作系统的一个特殊的 Java 类
    • 类中所有方法都是被 native 修饰
  • UnSafe 是一个 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地 native 方法进行访问 UnSafe 相当于一个后门,基于该类可直接操作特定的内存数据
  • UnSafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 语言的指针一样直接操作内存,因为 Java 中的 CAS 操作均依赖于 UnSafe 类中的方法
  • UnSafe 类中所有方法均被 native 修饰,也即 UnSafe 类中方法均直接调用操作系统底层资源执行相应任务
  • 变量 valueOffset,
  • 表示该变量值在内存地址中的偏移地址,
    • 因为 UnSafe 就是根据内存偏移地址获取数据的
  • 变量 value 用 volatile 修饰,保证了多线程之间内存可见性

getAndIncrement 方法

  • 多线程下 i++不安全,atomicInteger.getAndIncrement()如何保证线程安全?
  • CAS 的全称是 Compare-And-Swap,它是一条 CPU 并发原语
  • 用于判断内存中某个位置的值,是否为预期值,如果是更改为新的值,该过程是原子的
  • AtomicInteger 类主要利用 CAS 以及 volatile 和 native 来保证原子性操作,从而避免 synchronized 的高开销执行效率大为提升

  • 小李总结
    • CAS 是在并发写操作时提供一种类似乐观锁版本机制的自旋
    • volatile 是保证并发读取时对其他线程的可见性
    • native 则是执行原子性操作时,通过 UnSafe 对操作系统特定变量的操作加总线锁,锁开销远小于 synchronized
  • CAS 并发原语体现在 JAVA 语言中就是 sun.misc.UnSafe 类中的各个方法,
    • 调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现 CAS 的汇编指令,这是一种完全依赖于硬件的功能,通过它实现 了原子操作
  • CAS 是一种系统原语,原语属于操作系统的范畴,由若干指令组成,用于完成某个功能的过程
    • 原语执行必须连续,再执行过程中不允许被中断,也即 CAS 是 一条 CPU原子指令,能够保证数据一致性

new AtomicInteger().getAndIncrment();会发生什么?

  • 假设两个线程 A 和 B 同时执行 getAndAddInt 操作(分别泡在不同 CPU 上):
    • AtomicInteger 里面的 value 原始值为 3 即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程 A 和线程 B 各自持有一份价值为 3 的 value 副本,分别到各自的工作内存
    • 线程 A 通过 getIntVolatile(var1,var2)拿到 value 值 3,这时线程 A 被挂起
    • 线程 B通过 getIntVolatile(var1,var2)拿到 value 值 3,这时线程 B 没有被挂起,并且执行 compareAndSwapInt 方法进行比较,内存值也为 3,将 3 修改为 4
    • 此时 A 线程恢复,执行 compareAndSwapInt 方法进行比较,发现期望值 3 不等于内存值 4,该值被其他线程抢先修改,A 线程只能重新读取重来一次 CAS 自旋
    • 线程 A 重新获取 value 值,因为变量 value 被 volatile,所以其他线程对它的修改 线程 A 总能看到线程 A 继续执行 compareAndSwapInt 进行比较替换,直到成功

底层实现

native 方法表示底层方法
  • UnSafe 类中的 compareAndSwapInt 是一个本地方法,该方法实现位于 unsafe.cpp 中
代码语言:javascript
复制
UNSAFE_ENTRY(jboolean,Unsafe_CompareAndSwapInt(
        JNIEnv *env,jobject unsafe,jobject obj,jlong offset,jint e,jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
//获取变量value在内存中的地址,根据偏移量valueOffset,计算value的地址
jint *addr = (jint *)index_oop_from_field_offset_long(p,offset);
/*
    调用Atomic中的函数cmpxchg进行比较交换,参数x是要交换的值,e是要比较的值
        cas成功,返回期望值e,等于e,此方法返回true
        cas失败,返回内存中的value值,不等于e,此方法返回false
*/
return(jint)(Atomic::cmpxchg(x,addr,e)==e);
UNSAFE_END

JDK 提供的 CAS 机制,在汇编层级会禁止变量两侧的指令优化,然后使用 cmpxchg 指令比较并更新变量值(原子性)

(Atomic::cmpxchg(x,addr,e))==e;

cmpxchg

调用Atomic中的函数cmpxchg进行比较交换,参数x是要交换的值,e是要比较的值 return(jint)(Atomic::cmpxchg(x,addr,e)==e);

代码语言:javascript
复制
unsigned Atomic ::cmpxchg(unsigned int exchange_value,
                volatile unsigned int* dest,unsigned int compare_value)
{
    assert(sizeof(unsigned int)==sizeof(jint),"more work to do")
    //根据OS类型调用不同平台的重载函数,这个在预编译期间编译器会决定使用哪个平台下的重载函数
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value,(volatile jint*)dest,(jint)compare_value);
}
不同 OS 下调用不同的 cmpxchg 重载方法
代码语言:javascript
复制
inline jint Atomic::cmpxchg (int exchange_value, volatile jint* dest,jint compare_value){
//判断是否是多核cpu
int mp os::is _MP();
    _asm{
    //三个move指令表示的是将后面的值移动到前面的寄存器上
    mov edx, dest
    mov ecx,exchange_value
    mov eax, compare_value
    //cpu原语级别,cpu触发
    LOCK_IF_MP(mp)
    //比较并交换指令
    //cmpxchg:即"比较并交换"指令
    //dword:全称是doubleword表示两个字,一共四个字节
    //ptr:全称是pointer,与前面的dword连起来使用,表明访问的内存单元
    //将eax寄存器中的值(compare_value)与[edx]双字内存单元中的值进行对比。
        cmpxchg dword ptr[edx],ecx
    }
}
Summary

只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。 核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

何谓原子引用?

基于原子引用实现自定义原子类 example:AtomicBook,AtomicOrder,etc…

  • 通过一个包装类 AtomicRefence 进行实现
代码语言:javascript
复制
        AtomicReference<User> atomicReference = new AtomicReference<>();

        User z3 = new User("z3", 22);
        User li4 = new User("li4", 28);

        atomicReference.set(z3);

        System.out.println(atomicReference.compareAndSet(z3, li4) +
                "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, li4) +
                "\t" + atomicReference.get().toString());

CAS 与自旋锁

何谓自旋锁

  • 自旋锁

CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,自旋,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取这样的好处在于减少了线程上下文切换的消耗,弊端则是会消耗 CPU

  • 从 OpenJDK 的源码中查看 Unsafe.java
    • CAS 是实现自旋锁的基础,其本质的死循环,直至获取锁,当获取锁时返回 true 循环结束

手写一个自旋锁

代码语言:javascript
复制
 /**
     * description :
     * 实现一个自旋锁,复习CAS
     * 自旋锁的优点 : 循环比较没有类似wait的阻塞
     * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法将自己持有锁5秒,B随后进来后发现,
     * 但前线程持有锁,因此自能通过自旋等待,知道A释放锁后B随即抢到
     *
     * @param args
     */

    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t" + "--come in");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void unlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t" + "--task over unlock");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            spinLockDemo.unlock();
        }, "A").start();
        //保证A先于B启动
        try {
            TimeUnit.MICROSECONDS.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            spinLockDemo.lock();

            spinLockDemo.unlock();
        }, "B").start();

    }

CAS 的弊端

循环时间长开销很大

  • 当 getAndAddInt 方法执行时,陷入 do…while 循环中
  • CAS 失败,会一直进行尝试,若长时间一直不成功,必然会给 CPU 带来大量开销

引发 ABA 问题

问题描述(如何产生)

  • CAS 算法实现一个重要前提需要取出某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变化
  • 比如一个线程 1 从内存位置 V 中取出 A,此时另一个线程 2 也从内存中取出 A
    • 线程 2 进行了一些操作将值变成了 B,又将 V 位置的数据变为 A,
    • 此时线程 1 进行 CAS 操作发现内存中仍然是 A,符合预期,此后 A 也成功操作
  • 尽管线程 1 的 CAS 操作成功,但并不代表在此过程中就是正常的
解决方案

版本号时间戳原子引用

  • AtomicStampedReferenceDemo,单线程环境下
代码语言:javascript
复制
 public static void main(String[] args) {
        Book spring = new Book(1, "spring");
        AtomicStampedReference<Book> bookAtomicStampedReference = new AtomicStampedReference<>(spring, 1);
        System.out.println(bookAtomicStampedReference.getReference() + "\t" + bookAtomicStampedReference.getStamp());
        Book juc = new Book(1, "juc");
        boolean b = bookAtomicStampedReference.compareAndSet(spring,
                juc,
                bookAtomicStampedReference.getStamp(),
                bookAtomicStampedReference.getStamp() + 1);
        System.out.println(b + "\t" + bookAtomicStampedReference.getReference() + "\t" + bookAtomicStampedReference.getStamp());
        b = bookAtomicStampedReference.compareAndSet(
                juc, spring,
                bookAtomicStampedReference.getStamp(),
                bookAtomicStampedReference.getStamp() + 1);
        System.out.println(b + "\t" + bookAtomicStampedReference.getReference() + "\t" + bookAtomicStampedReference.getStamp());
    }
  • 模拟多线程环境下的 ABA 事件
代码语言:javascript
复制
    static AtomicInteger atomicInteger = new AtomicInteger(100);

    public static void main(String[] args) {
        new Thread(()->{
           atomicInteger.compareAndSet(100,101);
           try {
               TimeUnit.MILLISECONDS.sleep(10);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
            atomicInteger.compareAndSet(101,100);
        },"t1").start();
        
        new Thread(()->{
           try {
               TimeUnit.MILLISECONDS.sleep(200);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
            System.out.println(atomicInteger.compareAndSet(100, 2024) + "\t" + atomicInteger.get());
        },"t2").start();
    }
image.png
image.png
  • t1 线程睡眠 10 毫秒之后的操作使得过程发生变化,但是 t2 进行 CAS 过程照常进行,未察觉异常
  • 使用 AtomicStampedReference 解决 ABA 场景
代码语言:javascript
复制
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "首次版本号" + stamp);
            //暂停500毫秒,保证后面的t4线程初始化拿到版本与t3一致
            try {
                TimeUnit.MICROSECONDS.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第2次流水" + stampedReference.getStamp());
            stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第3次流水" + stampedReference.getStamp());
            }, "t3").start();
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "首次版本号" + stamp);
            //暂停1秒钟,等待上面的t3线程发生ABA问题
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            boolean b = stampedReference.compareAndSet(100, 2024, stamp, stamp + 1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
        }, "t4").start();
  • 在 t3,t4 同时获取资源后,t3 抢先对资源进行操作,完成之后将数据还原,但会留下邮戳即版本号
  • t4 进行资源操作时会查看版本号,版本号由由原来的 1 变为 3,t4 就无法进行对版本号 3 的编辑,即使获取到的数据是与版本号 1 一致,但 t4 所持版本号与最新版本号已经不同
  • 因此 AtomicStampedReference 类能够实现多线程环境下保证数据一致性,这种实现是基于 CAS 思想的硬件层级对 CPU 总线的加锁,与 Java 锁机制相比效率更高,但由于 CAS 的特点对其 CPU 的消耗也更大
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前置基础
  • CAS 出现前后
    • CAS 出现以前(不使用原子类)
      • CAS 出现之后(使用原子类)
        • 两种方式的比较
        • CAS 是什么
          • 简介
            • 实现原理
              • CaseDemo
                • 硬件级别对一致性的保证
                  • 对源码的理解
                  • CAS 底层原理与 Unsafe
                    • Unsafe 类
                      • getAndIncrement 方法
                        • new AtomicInteger().getAndIncrment();会发生什么?
                          • 底层实现
                            • native 方法表示底层方法
                            • cmpxchg
                            • 不同 OS 下调用不同的 cmpxchg 重载方法
                            • Summary
                        • 何谓原子引用?
                        • CAS 与自旋锁
                          • 何谓自旋锁
                            • 手写一个自旋锁
                            • CAS 的弊端
                              • 循环时间长开销很大
                                • 引发 ABA 问题
                                  • 问题描述(如何产生)
                                  • 解决方案
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档