并发-CAS

前言

CAS【Compare And Swap,乐观锁】是JUC的基础,学习CAS的原理以及使用。

一、案例

多线程环境下的失败案例:模拟100个用户同时访问网站10次:

public class Demo1 {

    static int count = 0;

    /**
     * 模拟用户访问服务器
     */
    public static void request() {
        try {
            //每个用户花费2ms访问本网站
            TimeUnit.MILLISECONDS.sleep(2);
            //网站访问加一
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        final int threadNumber = 100;
        //使得主线程只能在所有用户都执行完毕后才能执行
        CountDownLatch cdl = new CountDownLatch(threadNumber);

        long start = System.currentTimeMillis();

//        一百个用户同时请求
        for (int i = 0; i < threadNumber; i++) {
            new Thread(() -> {
            try {
                //每个用户请求十次网站
                for (int j = 0; j < 10; j++) {
                    request();
                }

            } catch (Exception e) {
                System.out.println(e.getCause());
            }finally {
                cdl.countDown();
            }
            }).start();
        }


        //等待其余线程执行完毕
        cdl.await();

        System.out.println("访问量:" + count);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

虽然执行速度很快,但是试了很多次,访问量都到不了1000,说明达不到要求,这样的代码就是失败的
在这里插入图片描述


分析原因:
执行引擎在计算count++时,并不是一个原子操作【即一个一气呵成,不被其他线程中断的操作】
实际上,count的执行分为三个步骤:
①从堆区获取count;【静态变量存放在堆区该类对象的位置】
②将count + 1,复制给一个临时变量t;
③将t赋值给count,写回。

由于三步不能一次执行完毕,在某个线程的执行过程中,可能出现其他线程也同样执行了一二两步,此时两者得到的t都是等值的,因此将t赋值给count的操作就重复进行了,浪费了一次增加访问量的机会【也可以说某一次修改被覆盖了】

解决办法:
通过上锁的方式,让执行count++的操作不被其他线程打断,只有彻底完成才会被其他线程继续执行,代码见下节

二、正常的同步

java提供了synchronized与ReentrantLock两种方式来实现加锁。

 public synchronized static void request() {
        try {
            //每个用户花费2ms访问本网站
            TimeUnit.MILLISECONDS.sleep(2);
            //网站访问加一
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

利用synchronized锁住request(),耗时如下:

在这里插入图片描述
可见,耗时翻了将近12倍。加锁的消耗巨大。

为什么耗时如此之高?
①加锁本身需要耗时
②中间的很多操作本来就无需加锁,强行加锁使得这些原本可以并发的行为也只能排队执行了。

既然有些操作不需要加锁,我们试试看只在其中的某个地方加锁看看:

   public static void request() {
        try {
            //每个用户花费2ms访问本网站
            TimeUnit.MILLISECONDS.sleep(2);
            //网站访问加一
            ReentrantLock lock = new ReentrantLock();
            lock.lock();
            count++;
            lock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

只比不加锁的情况慢一点点,果然效率提升了很多。
在这里插入图片描述

但是就前面分析的,就单从i++来说,只需要将最后赋值的操作加锁就行,可不可以进一步优化呢?
使用自定义的简单cas来实现。

在这里插入图片描述
在这里插入图片描述

三、模拟cas

这些操作中,只有count++的第三步赋值时需要加锁,因此使用比较交换思想,我们可以这么做:

  • 提供一个获取当前count的方法getCount(),由于不加锁,这个方法很快【①不加锁】
  • 定义一个cas方法,比较传递的exceptCount与当前实际的count是否相等,若相等就赋值【第③步,通过比较实现同步】,若赋值成功返回true
  • request()中,将count+1和自己当前获取到的count作为exceptCount传递给cms(),若返回false,说明不相等,再次重复获取count,执行赋值。
    这样通过比较、赋值的行为本身就比加锁的行为快很多,代码如下:

注意:cas()方法必须要加锁,保证在执行过程中count值不会被修改。

static int count = 0;

    /**
     * 模拟用户访问服务器
     */
    public static void request() {
        try {
            //每个用户花费2ms访问本网站
            TimeUnit.MILLISECONDS.sleep(2);
            //网站访问加一
//            定义期待值,表示当前该线程取得的count值
            int excpectCount;
            //比较为false,说明取得值已经变化,重新再取一次,直到成功
            while (!compareAndSwap(excpectCount = getCount(), excpectCount + 1)){}
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static synchronized boolean compareAndSwap(int exceptCount, int co) {
        int c;
        if ((c = getCount()) == exceptCount) {
            count = co;
            return true;
        }
        return false;
    }

    private static int getCount() {
        return count;
    }


在这里插入图片描述
也可以说非常快了。

四、JDK的cas api

jdk的cas是通过Unsafe类来实现的。Unsafe类可以操作本地内存,类似以前说过的allocateDirect()类型的方法。

Unsafe提供一些操作本地内存的本地方法,通过c语言实现,其中就宝座实现cas的几个方法。
cas实际上是一个操作系统的原子命令,我们平时说的cas实际上是前面的准备工作+实际的cas操作,狭义的cas特指将数据存放到对应的内存位置。
cas的原理是:当一个线程比较成功时,就会将总线上锁,使得其他线程无法通过总线去修改变量,这样当修改完毕之后,其他线程的比较就会失败,就去重新获取这个值。由于没有真实的加锁操作,效率很高。
在这里插入图片描述

我曾经在这篇关于ConcurrentHashMap的博客写了关于cms的相关api,以及通过反射的获取Unsafe对象的方式。


  1. CAS存在的问题:
    高并发情况下,会有多个线程都会成功的获取exceptCount与对等值的本地变量count进行操作,然而就算都能完整的做完,也只有一个线程可以进入执行cas替换掉内存的值,这样别的线程所做的事情就被浪费了
  2. ABA问题
    A在进行了获取except的时候没有cpu时间片了,会暂停执行;
    令一个线程修改了变量值,又给改回来了,当A重新执行时,比较会成功。
    相当于A遗失了两次更新的版本。

通过程序演示ABA:

public class Demo4 {
    public static AtomicInteger ai = new AtomicInteger(1);

    public static void main(String[] args) {
        new Thread(()->{
            try {
                int expect = ai.get();
                int aim = expect + 1;
                //等1s,让干扰线程执行修改
                Thread.sleep(1000);
                boolean isSuccess = ai.compareAndSet(expect, aim);
                System.out.println("cas成功了吗?" + isSuccess);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "main").start();

        new Thread(() -> {

            try {
                //若干扰线程先执行,让主线程执行到取得值的位置
                Thread.sleep(29);
                ai.set(2);
                System.out.println("干扰了执行");
                ai.set(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"干扰").start();
    }

程序中使用了AtomicInteger类,AtomicXXX类都在内部聚合Unsafe类,这是jdk提供给我们间接使用Unsafe类的一种方式。
在这里插入图片描述
main忽略了干扰线程的中间的ABA修改。

如何预防ABA问题?
一般情况下无需预防,在敏感的场合需要预防,可以使用synchronized代替Unsafe的cas操作,或者使用AtomicStapedReference类来解决。
这个类会在每次修改时同步修改版本号,若版本号不一致,即使值一致比较也会失败。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值