【倔地求升】CAS底层原理探寻

本文探讨了CAS(Compare and Swap)在Java中的应用,以及其底层的CPU硬件支持。通过乐观锁的概念,解释了CAS如何在多线程环境下保证数据一致性。CAS使用Unsafe类和native方法实现,依赖于CPU的cmpxchg指令。文章还涉及到了总线锁、缓存锁和MESI协议,以及volatile关键字在保证可见性和禁止指令重排方面的角色。
摘要由CSDN通过智能技术生成

【关键词】CAS,CPU,底层,缓存锁,MESI缓存一致性协议,总线锁,指令重排,volatile

前阵子看到一个面试题:

CAS底层原理是什么?

略作思考( ̄. ̄)

不就是相当于乐观锁嘛?

乐观锁与悲观锁?

例如:张三和李四去买东西的时候,但是东西只有一个了,怎么买?

店家这时候拍拍脑袋( ̄︶ ̄)↗,想到一个办法:

我就一个人,谁来我这里我就先卖给谁。就很棒,换在计算机里,这属于叫悲观锁,在卖东西时,也就是请求资源的时候,资源只有唯一一个人才能获取,其他人都被阻塞起来不让获取

悲观锁:简单讲就是用阻塞,加锁的方式,在一定时间内,某些东西只让一个人访问或只让一个人操作,其他线程操作将被堵住,不允许操作

过了阵子,店家生意越来越好,在村里开始开分店了,但是仓库还是共用一个的,这个时候,张三李四又来买东西了,老板和分店老板跑到仓库里去看商品,商品还在\( ̄︶ ̄)/

老板眼疾手快把商品拿走了,留下分店老板一人在风中凌乱,,

分店老板看没有商品,只好回去,然后每隔几个小时就会跑来仓库看看是否有他需要的商品,有就拿走,这也就是乐观锁了

乐观锁(CAS):例如线程1和线程2同时在修改一个值,线程1和线程2去修改的时候,发现这个文件是1号的版本,这时线程1抢先一步,将文件替换成2号的版本了,线程2想要替换的时候发现,该文件已经是2号版本了,中途已经被别人替换过了,这时线程2如果想替换就得重新来获取该文件的状态,只有当版本号对应上的时候,才允许替换成功

乐观锁也被成为CAS (compare and swap),在java里面是怎么实现的呢?

常用到的一些地方:

ReentrantLock 类:
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
ReentrantLock在底层对某个内容尝试加锁的时候,就会调用Sync类,而这个类继承于抽象类:AbstractQueuedSynchronizer

AQS在对对象加锁的时候,首先会进行尝试去获取锁,而获取锁这个方法,采用的就是CAS,尝试获取锁,如果获取得到,则进行加锁,获取不到,则继续获取,进行自旋操作

继续点开,我们可以发现,这个CAS实际是使用的更底层的Unsafe类去完成的CAS操作(当然不仅仅是AQS,Atomic系列的CAS操作使用的也是这个)

再点开Unsafe类,我们可以看到:

    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);

满满一排的native方法,我这里就列出我们看的这几种CAS方法,而Unsafe底层采用的native方法是直接由cpu硬件支持的原子操作,这使得java程序通过CAS来运行的效率会非常高。

再往底层去查看就是C语言代码了:

  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 根据偏移量valueOffset,计算 value 的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用 Atomic 中的函数 cmpxchg来进行比较交换
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

上面的代码大概含义:

首先使用上面的jint计算了value的地址,然后根据这个地址,使用了Atomiccmpxchg方法进行比较交换。现在问题又抛给了这个cmpxchg,真实实现的是这个函数,接着往下:

    assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  /*
   * 根据操作系统类型调用不同平台下的重载函数,
     这个在预编译期间编译器会决定调用哪个平台下的重载函数
  */
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, 
                     (volatile jint*)dest, (jint)compare_value);

继续往下:

  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }

这块就开始涉及到汇编语言的问题了, 首先三个move指令表示的是将后面的值移动到前面的寄存器上。然后调用了LOCK_IF_MP和下面cmpxchg汇编指令进行了比较交换。现在我们不知道这个LOCK_IF_MPcmpxchg是如何交换的,没关系我们最后再深入一下:

  //1、 判断是否是多核 CPU
  int mp = os::is_MP();
  __asm {
    //2、 将参数值放入寄存器中
    mov edx, dest   
    mov ecx, exchange_value
    mov eax, compare_value 
    //3、LOCK_IF_MP指令
    cmp mp, 0
    //4、 如果 mp = 0,表明线程运行在单核CPU环境下。此时 je 会跳转到 L0 标记处,直接执行 cmpxchg 指令
    je L0
    _emit 0xF0
//5、这里真正实现了比较交换
L0:
    /*
     * 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释:
     *   cmpxchg: 即“比较并交换”指令
     *   dword: 全称是 double word 表示两个字,一共四个字节
     *   ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元 
     * 这一条指令的意思就是:
       将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行对比,
       如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。
     */
    cmpxchg dword ptr [edx], ecx
  }

到这里,就可以很明显的看出来,CAS在C处就是由操作系统的汇编指令完成的,但是CPU里是如何实现CAS操作的原子性等等呢?

在之前,关于CAS指令最著名的传闻是CAS需要锁总线,因此CAS指令不但慢而且会严重影响系统并发度,即使没有冲突是也一样。不过在较新的CPU中(对于Intel CPU来说是486之后),已经不存在说因为CAS指令特别慢而影响系统并发度了,在这里需要提一下操作系统的的总线锁和缓存一致性。

总线锁、缓存锁

对于使用CAS,在汇编语言里使用cmpxchg指令,先判断cpu是否为单核cpu,如果是单核cpu,则利用总线锁, 保证操作的原子性,下面列一个图来讲一下cpu在内存和缓存的操作结构:

名词解释下:L1d 数据缓存    L1i指令缓存

总线锁:就是在cpu操作主存的时候,当一个cpu核想要去执行一个任务的时候,它会向总线发送一个LOCK信号,总线收到LOCK信号之后,如果此时其他线程想要去请求总线,就会被阻塞住,老版本CPU使用的这种策略,但是由于这种类似“悲观锁的”的机制,会导致其他线程被阻塞,效率大大降低,后续的CPU也没有采用这种策略

缓存锁:如果内存区域某个数据,已经被两个或以上的处理器核缓存了,缓存锁就会通过缓存一致性,阻止对其修改,来保证操作的原子性,当其他处理器核对该内存进行修改后,其他核的缓存中该数据将被无效化。

所以很显然,总线锁的性能没有缓存锁性能好,而缓存锁采用的缓存一致性,就是现在大家听得比较多的MESI协议:

M(Modify):修改,表示该内存空间已被修改过

E(Exclusion):独占,表示该内存空间,仅此核拥有这个内存空间的缓存,就比如,内存空间A,现在只有核1缓存起来了,其他核都没有缓存该内存空间

S(Shared):共享,表示该内存空间目前有多个核均有该内存空间的缓存

I(Invalid):无效,表示该内存空间在缓存里的储存是无效的

缓存行:在CPU缓存里,由于缓存使用时,缓存认为你使用数据时,大概率会使用地址相邻的数据,故一次性将一小块连续空间的内容全部读取出来了,在cpu里,这一块空间,常见大小为64字节,也就是说,缓存读取和操作的最小单元是64字节,其中也会出现缓存行伪共享(CPU缓存行 - 简书)、Java使用时也有缓存行对齐等问题,这里就不一一详细展开讲了

上述流程大概是这样的,当核1去拿数据A的时候,从L1查询数据A,查了发现L1中没有缓存A,这时便跑到L2里去查询,L2也没有,接着又跑到L3里去查询,这时L3也没有

(缓存读取效率:L1:1ns左右            L2:3ns左右            L3:15ns左右            主存:60-80ns左右)

实在没办法,核1只能去主存里拿,它去主存拿完了之后,又不厌其烦的在回来时,先把数据A放到L3,再放到L2,再放到L1中,都放完了之后,cpu会把这个内存空间标记为Exclusion,这个内存空间是被他独享的

叮叮叮

系统这时候发了一个通知,核2也拿了数据A,核1没有办法,只能把数据A的状态更新为Shared,表示该内存已经被多个核所共享

这时候,核2在拿了数据A后,对数据A做了修改,然后将修改后的内容放入主存中,主存被修改后,系统便发送了个通知,核1在接收到通知后,便将自己缓存的数据A标识位Invalid,在需要重新使用该数据的时候,再重新去主存拿。

【延伸】volatile

而Java中的volatile在使用的时候,是有2个用处的:

1.保证变量可见性

说到缓存一致性,就要谈到volatile,在java中,一旦变量使用volatile,就会给这个变量发送一条LOCK前缀的指令,这个指令会将这个变量所在缓存行的数据写回到系统内存

而其他核在看到这个LOCK指令的时候,就不会使用缓存内的数据,而是每次使用该数据都会去主存内重新读取,这样一来,尽管效率变低,但是变量的可见性却保证了

2.禁止指令重排

首先第一个问题,什么是指令重排?

指令重排:cpu在执行指令的时候,并不一定会按照程序的顺序去执行

比如你程序有10行,逻辑1:计算        逻辑2:IO读写,这个时候,如果两者是毫无关联的,那么逻辑1与逻辑2是可以打乱顺序执行的,好处在于,有时候乱序执行 ,会提升系统性能,比如此时先派一个程序去执行逻辑2,在IO读写的延时中,可以利用其它核去执行逻辑1里的计算功能,这样,可以减少停顿时浪费的时间,从而提升效率

那么,cpu是怎么做到,让代码可以打乱顺序执行,但是又不影响逻辑性呢?

cpu可以利用依赖分析,对每行代码所依赖的其他行进行分析,给定范围内的指令构成多根树,从根开始排,就类似于JVM里的GC回收算法,GC在回收之前是有一个可达性分析的,类似于这种,分析哪些代码是相互关联,相互依赖的,将代码分成几块,而每一块之间不相互依赖,这样就可以将每一块的执行顺序进行打乱了

(不过当你的机器指令窗口足够大的时候,编译器可以不排)

那么回到正题

这里的禁止指令重排,其实就是让含有volatile的变量不会被重排序,是按照顺序去执行的,可以理解为把volatile变量相关的代码形成了一个指令关联树,从而达到禁止指令重排的目的

内容是工作1年时发布的,有什么不对的地方还望各位大佬给指出🤝🤝🤝🤝🤝

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值