JDK的CAS机制

1、CAS的引出

在理解什么是CAS之前让我们来看一段代码:

public class Demo01 {

    /**
     * 记录访问次数
     */
    static int count;

    public static void request() throws InterruptedException {
        //调用一次耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        int threadSize = 100;

        /**
         * 为了保证100线程全部执行,所以使用CountDownLatch栅栏类
         */
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        for(int i = 0 ; i< threadSize ; i++){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        for (int i = 0 ; i< 10 ; i++){
                            request();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        countDownLatch.countDown();
                    }
                }
            });
            thread.start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("当前线程:"+Thread.currentThread().getName() + "耗时:"+(endTime-startTime)+"ms,count:"+count);
    }
}

上面的代码的说明:在多线程情况下,访问一个方法,访问成功一次则count++,共有开启100条线程,每个线程访问10次,count的最终结果预期为1000.
最终程序运行结果为:
在这里插入图片描述
我们通过结果发现,访问数并你没有达到我们预期的1000,运行多次后count的结果依旧是<1000的。
那这是为什么呢?
我们不难发现,count++ 不是原子性操作,它实际上是由三步来完成的!

  1. 获取count的值 ,记做A : A = count
  2. 将A的值+1 ,记做B : B = A + 1
  3. 将B的值赋值给count

当有多个线程同时执行count++,他们同时执行到上面步骤的第一步,得到count是一样的,3步操作结束后。count只加1,导致count结果不正确

基于这个问题,我们可以对count++操作的时候,我们让多个线程排队处理,多个线程同时到达request()方法的时候,只能允许一个线程可以进去操作,其他线程在外面等待,等里面的线程处理完毕后,外面等待的线程再进去一个,这样操作的count++就是排队进行的,结果一定是正确的。
我们只需要在当前代码上稍微做点修改即可,其他代码不变,在request()方法上添加synchronized 关键字,锁对象为当前类的Class对象。

  public synchronized static void request() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }

我们再次执行代码,结果是:
在这里插入图片描述
通过结果我们不难发现,count确实达到了我们预期的结果,但耗时太长了,因为线程中的request方法使用synchronized关键字修饰,保证了并发情况下,request方法同一时刻只允许一个线程进入,request加锁相当于串行执行了,所以才会出现耗时长。

怎么解决这耗时长的问题呢?
在之前我们提到过,count++实际上是由三部分组成的,

  1. 获取count的值 ,记做A : A = count
  2. 将A的值+1 ,记做B : B = A + 1
  3. 将B的值赋值给count
    接下来,我们将升级第三步,将第三步分为四个步骤去完成,分别是
    - [1] 获取锁
    - [2] 获取当下count的最新值,记做expectCount
    - [3] 判断expectCount的值是否等于A,如果相等,则将B的值赋值给count,并返回true,否则返回false,然后再次执行count++操作,直到返回true为止。
    - [4] 释放锁

代码如下:

 /**
     * 记录访问次数
     * volatile : 表示多线程之间,count值可见
     */
    volatile static int count;

    public static int getCount() {
        return count;
    }

    public static void request() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(5);
//        count++;
        int expectCount;
        while(!CompareAndSwap(expectCount = getCount(), expectCount+1)){}
    }

    /**
     * 只给count++的第三步加锁
     * @param expectCount 期望值
     * @param newCount 需要给count赋值的新值
     * @return 成功返回true 失败返回false
     */
    public static synchronized boolean CompareAndSwap(int expectCount , int newCount){
        if(expectCount == getCount()){
            count = newCount;
            return true;
        }
        return false;
    }
     public static void main(String[] args) {
    	//main方法不做改变
    }

执行代码,结果是:
在这里插入图片描述
从结果中,我们可以很直接的发现,耗时变短了,count的值也是符合我们预期的值,这就是CAS。

2、介绍CAS机制

CAS(Compare-And-Swap):比较再替换。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。CAS包含三个操作数 (V(内存位置)、A(期望值)、B(新值)),如果内存位置V的值与期望值A相同,那么处理器判断内存某个位置的值是是否为预期值,如果是则更改为B的值,如果不是,处理器不会做任何操作,并且这个过程是原子的。

3、JDK中是如何实现CAS呢?

以比较简单的AtomicInteger为例(一些AtomicInteger的方法):
在这里插入图片描述
在这里插入图片描述
我们发现,AtimicInteger的方法都是通过unsafe对象来调用的。
Unsafe是CAS的核心类,由于Java是无法直接访问底层系统的,需要通过本地(native)方法来防访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。在Java中CAS操作的执行依赖于Unsafe类的方法。
在这里插入图片描述Unsafe的方法都是由native修饰的,功力还比较浅薄,等高深一点再进去看原理吧!

总结:JDK实现CAS机制是因为底层调用了Unsafe的CAS方法

4、CAS机制的优缺点

优点:CAS是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁,什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。

缺点:
1.CPU开销较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.ABA问题:CAS最大的问题。CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,在CAS方法执行之前,被其它线程修改为了B、然后又修改回了A,那么CAS方法执行检查的时候会发现它的值没有发生变化,但是实际却变化了。这就是CAS的ABA问题。

5、代码模拟ABA问题

public class CASABATest {

    /**
     * AtomicInteger: 可以用原子方式更新的 int 值
     *
     * 准备数据 , 初始值为1
     *
     */
    public static AtomicInteger a = new AtomicInteger(1);

    public static void main(String[] args) {
        /**
         * 创建两个线程,模拟CAS的ABA问题
         * 主线程执行+1操作
         * 其他线程在主线程替换旧值时执行+1,-1操作
         */
        Thread main = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("操作线程:"+Thread.currentThread().getName()+",初始值:"+a.get());
                int expectNum = a.get();
                int newNum = expectNum + 1;

                try {
                    Thread.sleep(1000L); // main线程休息一秒,让出cpu的资源
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean isCASSuccess = a.compareAndSet(expectNum, newNum);
                System.out.println("操作线程:"+Thread.currentThread().getName()+",[incread],CAS:"+isCASSuccess);

            }
        },"主线程");

        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10L);//休息10毫秒,保证主线程运行到了sleep
                    int incNum = a.incrementAndGet(); // +1 操作
                    System.out.println("操作线程:"+Thread.currentThread().getName()+",【increment】值为:"+a.get());
                    int decNum = a.decrementAndGet(); // -1 操作
                    System.out.println("操作线程:"+Thread.currentThread().getName()+",【decrement】值为:"+a.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"其他线程");

        main.start();
        other.start();
    }

}

代码分析:创建两个线程,主线程执行+1操作,干扰线程执行+1,-1操作,然后通过AtomicInteger的compareAndSet() ->替换成功则返回true,否则返回false。
运行代码,结果如下:
在这里插入图片描述
我们看到CAS的结果为true,但我们已经在干扰线程中对当前值进行了操作,这就意味着CAS并没有察觉到ABA问题,这就是CAS的ABA问题。

6、JDK自带的ABA问题解决方法

AtomicStampedReference主要包含一个对象引用及一个可以自动更新的整数“stamp”的Pair对象来解决ABA问题。
在这里插入图片描述
修改数据需要提供原来数据的引用以及版本号,只有版本号一致了,你就能去修改数据。我们去看一下他的compareAndSet()方法。
在这里插入图片描述
当你的期望引用以及你的期望版本号与Pair对象中的引用数据以及版本号一致,才可进行替换操作。
在这里插入图片描述
casPair()的底层源码:
在这里插入图片描述
ABA问题的解决,代码如下:

public class CASABATest02 {

    /**
     * AtomicInteger: 可以用原子方式更新的 int 值
     *
     * 准备数据 , 初始值为1
     *
     */
    public static AtomicStampedReference<Integer> a = new AtomicStampedReference<>(1,1);

    public static void main(String[] args) {
        /**
         * 创建两个线程,模拟CAS的ABA问题
         * 主线程执行+1操作
         * 其他线程在主线程替换旧值时执行+1,-1操作
         */
        Thread main = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("操作线程:"+Thread.currentThread().getName()+",初始值:"+a.getReference());
                Integer expectReference = a.getReference(); //期望引用
                Integer newReference = expectReference + 1; //新的引用
                Integer expectStamp = a.getStamp();//期望版本
                Integer newStamp = expectStamp + 1;
                try {
                    Thread.sleep(1000L); // main线程休息一秒,让出cpu的资源
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean isCASSuccess = a.compareAndSet(expectReference, newReference,expectStamp,newStamp);
                System.out.println("操作线程:"+Thread.currentThread().getName()+",[incread],CAS:"+isCASSuccess);

            }
        },"主线程");

        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10L);//休息10毫秒,保证主线程运行到了sleep
                    a.compareAndSet(a.getReference(),a.getReference()+1,a.getStamp(),a.getStamp()+1);//加1
                    System.out.println("操作线程:"+Thread.currentThread().getName()+",【increment】值为:"+a.getReference()+"版本:"+a.getStamp());
                    a.compareAndSet(a.getReference(),a.getReference()-1,a.getStamp(),a.getStamp()+1);//-1
                    System.out.println("操作线程:"+Thread.currentThread().getName()+",【decrement】值为:"+a.getReference()+"版本:"+a.getStamp());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"其他线程");

        main.start();
        other.start();
    }

}

代码分析:我们将AtomicInteger 对象替换成了AtomicStampedReference对象,给期望引用添加了期望版本号,在执行CAS操作时,如果期望引用和新引用或者期望版本和新版本不一致,compareAndSet()方法就会返回false,很显然,我们在干扰线程中对版本进行了两次+1操作,而期望版本是一次+1操作,所以一定返回的是false。
结果如下:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值