大厂30K面试题(一)请描述CAS的底层是如何实现的?一百度阿里

CAS的底层实现

CAS: compare and swap(compare and exchange),比较并交换,读取内存中的数值,计算修改后,将修改前的值与内存值比较,如果相同,则说明没有其他线程修改过,就将修改后的值写入内存;如果不同,则说明被改过,重复上述过程,直到写入成功,听着有点蒙,没关系,看一张图:

这是干嘛用的?其实它是用来替换以前的重量级锁的,说到这,又会提到一个概念,叫重量级锁,在jdk早期,synchronized本身都是用重量级锁实现,到后来jdk1.6才做了优化完善,所以你要想了解synchronized底层原理,先从重量级锁学起。

重量级锁它重在什么地方,平时我们写个锁synchornized(o)很简单,为什么说是重量级锁?

要了解它首先你得了解操作系统,在早期的操作系统,应用层是可以直接访问系统底层硬件的(比如内存、显卡等),所以操作不当就会经常把操作系统搞蓝屏,后来操作系统为了避免这个问题,不允许你应用层直接访问我硬件,必须经过我操作系统内核进行调度。也叫做用户态和内核态。当JVM里的一段Java代码想要申请一把操作系统的锁,对于操作系统而言,你jvm就是个普通的应用程序,必须要向我操作系统申请,经过操作系统的申请做线程调度的,我们叫它重量级。

你可能会有疑问,经过操作系统内核的就叫重量级?哪体现出来的?

这里有段汇编代码

;hello.asm
;write(int fd, const void *buffer, size_t nbytes)

section data
    msg db "Hello", 0xA
    len equ $ - msg

section .text
global _start
_start:

    mov edx, len
    mov ecx, msg
    mov ebx, 1 ;文件描述符1 std_out
    mov eax, 4 ;write函数系统调用号 4
    int 0x80

    mov ebx, 0
    mov eax, 1 ;exit函数系统调用号
    int 0x80

这里有个著名的0x80中断,申请个锁惊动系统内核80中断,这叫做重量级。

相反,不经过操作系统内核就能在用户层自行解决的叫轻量级锁

CAS究竟干了什么事呢?举个例子,假如有两个线程想对一个值【V】进行+1操作,第一个进程进来之后,先把这个值V【0】取出来,进行+1操作,然后再把+1后的结果【1】写回去之前,我先判断这个值是否依然为0,如果我发现这个值不是0了,变成【2】了,那么我就知道这肯定是有其他线程将这个值改掉了,这时候我不去写这个值,而是把这个值【2】再次读出来,再进行+1操作,写之前我判断这个值V是否为【2】,如果不为2,我就一直循环下去,直到成功为止。这个过程是不需要加锁的,所以说CAS叫无锁,又叫自旋锁。

看到这你可能有个疑问,如果我这个线程将值改成【1】后,在我往回写的过程之中,有一个线程将这个值【0】取走了,改成【1】,写回去,又有一个线程将值【1】取走了,改成了【0】,这个值从0 -->1 --> 0,如果是简单的值从0到1又回到0,这种没问题,不影响最终的结果,但是如果值是引用类型,不允许中间有人改过,那这就有问题,这就是CAS的ABA问题。

怎么解决?加版本号,举个例子,比如说你和你的女朋友分手了,分手之后她又经历了别的男人,又回到了你的身边,但是你发现她和以前不太一样了,那么你怎么察觉这一点?很简单,她离开的时候在她脑门上贴个标签1.0,当她再回来的时候,发现脑门上是99.0,那你就知道她到底经历了什么。

如果问你JDK具体的实现,BooleanReference, 用布尔值标注它有没有改变或版本号version标注。

看到这你可能又会问,你CAS不是比较并交换吗,如果一个线程比较完之后,将要写回的过程,另一个线程将这个值改了,那你不是傻了吗。其实最后这一步也就是写回的操作是原子操作,是不允许打断的,那么这个原子操作是怎么实现的呢?

如果我想实现一个功能,对一个值进行自增操作,创建100个线程,每个线程都对这个值进行10000次自增,那么期待值就是1000000,先用synchronized实现:

private static int m = 0;
public static void main(String[] args) throws Exception {
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);
    Object o = new Object();
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> {
            synchronized (o) {
              for (int j = 0; j < 10000; j++) {
                m++;
              }
              latch.countDown();
            }
      });
    }
    Arrays.stream(threads).forEach(Thread::start);
    latch.await();
    System.out.println(m);
}

用CAS实现:

private static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);
    for (int i = 0; i < 100; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                m.incrementAndGet();
            }
        });
      	latch.countDown();
    }
    Arrays.stream(threads).forEach(Thread::start);
    latch.await();
    System.out.println(m);
}

AtomicInteger底层都是CAS实现的,CountDownLatch叫门栓,latch.await();的意思就是把门栓打开,把门堵住,想进来必须拿到锁,必须等签名100把锁latch.countDown()到0了才能执行下面的操作,等同于thread.join();说跑题了,这里的核心是m.incrementAndGet();,它没有加锁是怎么保证数据一致性的?简单,进源码看下:

		/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

你会发现它调用了unsafe类的getAndAddInt方法,点进去:

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,这个方法怎么看着那么熟悉,What?这不就是CAS吗?点进去:

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

很不幸,只能到这里了,看到了native,这是调用了C++代码的实现,如果你想继续往下看,那你只能看UnSafe的底层C++代码了,上网搜unsafe.cpp compareAndSetInt代码实现:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
    oop p = JNIHandles::resolve(obj);
    //获取对象的变量的地址
    jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
    //调用Atomic操作
    return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END

发现最终调用了cmpxchg方法

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

这里已经是底层汇编实现了,看到LOCK_IF_MP,如果mp为true,就Lock,那么我们看下is_MP方法

  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

看到_processor_count != 1进程数不等于1,什么意思呢,就是不是单核的,再看下LOCK_IF_MP方法

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

从注释翻译出来, 添加一个“lock”的前缀,在哪添加呢?在某一指令上,MP的意思是multi processor,多核机器,也就是在多核上添加一个lock的前缀。所以最终汇编调用的指令是lock cmpxchg,这条汇编的指令的作用就是锁总线。锁总线什么意思?如果多CPU也就是多核,未防止我写入的时候被其他CPU打断,我把CPU通往内存的一组线给锁定,不允许其他CPU去修改。

所以CAS的底层实现就是lock cmpxchg,多核才会加锁,单核不需要。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoungJ5788

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值