Jave 面试 CAS就这?底层与原理与自旋锁

好兄弟们,不会真有人看不懂CAS吧?反正我是没看懂…

一. CAS是什么?

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 1. CAS是什么? => compareAndSet 比较并交换
 */
public class CASTest {

    public static void main(String[] args) {

        AtomicInteger atomicInteger=new AtomicInteger(5);

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

        System.out.println(atomicInteger.compareAndSet(5,2020)+"\t "+"currentData: "+atomicInteger.get());
    }
}

周阳老师的图就是画的骚~

在这里插入图片描述
怎么个意思呢

  • 一开始,我给主物理内存设置值为5
  • 第一个线程来了,要跟内存比较并交换,线程的期望值是 5 ,而刚好内存值就是5, 然后就交换了值,也就是把主物理内存的值5改为了2019,然后返回了个true代表取到的值与期望值是一样的
  • 然后通知其他线程可见了,第二个线程来了,发现主物理内存是2019,跟自己的期望值5不一样啊,然后就返回了个false,主物理内存并没有改变~.

二. CAS底层原理

首先来看看atomicInteger.getAndIncrement()为什么不加synchronized也能在多线程下保持线程安全
在这里插入图片描述
点开后,我们发现有个unsafe类,unsafe是CAS的核心类

  • 1. Unsafe
    是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于-一个后门,基于该类可以直接操作特定内存的数据。Unsafe类 存在于sun.misc包中,其内部方法操作可以像C的指针一-样直接操作内存,因为Java中Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  • 2. 变量valueOffset, 表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

  • 3. 变量value用volatile修饰, 保证了多线程之间的内存可见性。

2.1 JMM内存模型(涉及到的知识点)

由于JMM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

在这里插入图片描述

2.2 CAS底层

CAS的全称为Compare-And-Swap,它是一-条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会 造成所谓的数据不一致问题。

在这里插入图片描述
首先, var1代表当前对象,var2代表对象的偏移地址,var4就是那个+1的值,然后getIntVolatile()这个方法去获取当前对象的这个值是多少,给他保存到var5,然后,过了一会,compareAndSwapInt()这个方法去再比较当前对象的值还是不是var5,是的话就给这个值+1,返回true,while里面就是false就退出循环,最后返回出+1后的值. 如果当前对象不是之前的var5了,返回一个false,while循环里面就是true,继续循环,拿到下一个值去比较,直到比较成功~

再来一遍,根据JMM内存模型来看
在这里插入图片描述
在这里插入图片描述

  • 1. 假设有两个线程AB,根据上面的内存模型来看 AtomicInteger 里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存。
  • 2. 线程A通过getIntVolatile(var1, var2)拿到value值5, 这时线程A被挂起。
  • 3. 线程B也通过getlntVolatile(var1, var2)方法获取到value值5, 此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。
  • 4. 这时线程A恢复,执行compareAndSwapInt方法比较, 发现自己手里的值数字5和主内存的值数字6不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取新来一遍了。
  • 5. 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
2.3 总结与应用

1. CAS (CompareAndSwap)总结

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,
否则继续比较直到主内存和工作内存中的值一致为止.

2. CAS应用

CAS有3个操作数,内存值V;旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。.

3. CAS缺点

  • 由于用了while循环,当在线程数比较大的情况下,如果失败可能会出现一直循环,导致CPU过高
  • 只能保证一个共享变量的原子操作。
  • 引出来ABA问题(反正就是狸猫换太子把戏)

三. 自旋锁SpinLock

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

写了这么久代码,这不就是个自旋锁嘛!

代码贴出来

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 自旋锁Demo
 */
public class SpinLockDemo {

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

    public void myLock(){
       Thread thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t "+"come in ^0^");

        while(!atomicReference.compareAndSet(null,thread)){

        }
    }

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

    public static void main(String[] args) {

        SpinLockDemo spinLockDemo=new SpinLockDemo();

        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        },"AAA").start();

        try {
            TimeUnit.SECONDS.sleep(1); //保证线程绝对运行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1); //加锁后延迟1s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        },"BBB").start();

    }
}

怎么硕呢,大家看懂了以上代码就理解了什么是自选锁了

  • 这里我开始拿了个原子线程
  • 定义一个加锁方法,是自旋锁,如果当前线程是空就把当前线程更新进去CAS,并且跳出循环,如果当前线程不是空,就会一直在while循环里一直判断
  • 定义一个解锁办法,获取当前线程,如果原子线程还是当前线程,那就把它设置为null
  • 主方法中,我先让线程AAA获取锁,并且sleep5秒钟,这个时候当前线程就会一直是AAA线程
  • 然后BBB线程进来了,发现当前线程并不是null,而是AAA,就回一直循环判断当前线程什么时候为null,等到5秒后,线程AAA解锁了把当前原子线程释放掉了,这时候BB就拿到锁了,然后跳出循环,最终解锁~

唉…刚写完了!别白嫖啊,点赞关注,给你们福利啊~~转载请标注!

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值