带你尝试手写模拟 cas 及其应用场景

1 cas What?

cas是compareandswap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。

2 引出 CAS

需求:
开发一个网站,对访问量进行统计,用户每发送一个请求,访问量+1,如何实现?
模拟100个用户同时访问,并且每个人对咱们的网站发起10次请求,最后的总访问次数100*10=1000

2.1 示例1(线程不安全,结果不正确)

代码

public class CAS01 {
    // 总访问量
    static int count = 0;

    // 模拟访问的方法
    public static void request() throws InterruptedException {
        // 模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }


    public static void main(String[] args) throws InterruptedException {
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        // 开始时间
        Long startTime = System.currentTimeMillis();
        // 模拟100个用户
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                // 每个线程就是一个用户
                // 模拟每个用户发起十次访问请求
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 每个线程结束之后 threadSize -1
                    countDownLatch.countDown();
                }

            });
            thread.start();
        }
        // 保证100个线程结束之后,再执行后面的代码
        countDownLatch.await(); // threadSize==0 执行后面的代码
        Long endTime = System.currentTimeMillis();
        System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
    }
}

结果

image-20210329205540052

分析

Q: 为什么count!=1000,问题出在哪里了?

A: count++ 操作实际上是由3步来完成

1.获取 count 的值,记做 A : A = count;

2.将 A 的值 +1,得到 B:B=A+1

3.将B的值赋值给 count

如果有两个线程同时执行 count++,两个线程执行到上面步骤第一步,得到的count 的值是一样的,3步操作结束后,count 只有+1,导致count结果不正确!

Q: 怎么解决结果不正确问题?
A: 对 count++ 操作的时候,我们让多个线程进行排队,多个线程同时到达 request() 方法的时候

只能允许一个线程可以进行操作,这样操作的 count++ 就是排队进行的,结果一定是正确的

Q: 怎么实现排队效果?

A: Java 中 synchronized 关键字和 ReentrantLock 都可以实现对资源加锁,保证并发正确性。

2.2 示例2 synchronized 保证线程安全

代码

public class CAS02 {
    // 总访问量
    static int count = 0;

    // 模拟访问的方法
    // synchronized 修饰static request()方法,修饰的是类对象,锁的是类
    public static synchronized void request() throws InterruptedException {
        // 模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }


    public static void main(String[] args) throws InterruptedException {
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        // 开始时间
        Long startTime = System.currentTimeMillis();
        // 模拟100个用户
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                // 每个线程就是一个用户
                // 模拟每个用户发起十次访问请求

                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 每个线程结束之后 threadSize -1
                    countDownLatch.countDown();
                }

            });
            thread.start();
        }
        // 保证100个线程结束之后,再执行后面的代码
        countDownLatch.await(); // threadSize==0 执行后面的代码
        Long endTime = System.currentTimeMillis();
        System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
    }
}

结果

image-20210329210118319

分析

发现用 synchronized 关键字虽然完成了线程同步,成功输出 1000,但是发现十分耗时,说明 synchronized 关键字十分浪费资源,很大程度上锁住了一些没有不要锁的地方。

Q: 耗时太长的原因是什么?

A: 程序中 的request方法使用 ``synchronized 关键字修饰,保证了并发情况下的线程安全问题。但是这样子的加锁使得相当于串行执行了。

Q: 如何解决耗时长的问题?

A: count++ 操作实际上是由3步来完成

1.获取 count 的值,记做 A : A = count;

2.将 A 的值 +1,得到 B:B=A+1

3.将B的值赋值给 count

升级第三步的实现

  1. 获取锁
  2. 获取以下 count 最新的值,记做 LV
  3. 判断LV 是否等于 A ,如果相等,则将 B的值赋值给 count ,并返回 true 否则 返回 false
  4. 释放锁

这样就会使得锁的范围就是第三步,所系了范围。

2.3 示例3 模拟CAS 保证线程安全

代码

public class CAS03 {
    // 总访问量
    volatile static int count = 0;

    // 模拟访问的方法
    public static void request() throws InterruptedException {
        // 模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        int expectCount;  // 表示期望值
        while (!compareAndSwap(expectCount = getCount(), expectCount + 1)) {
        }
    }


    public static void main(String[] args) throws InterruptedException {
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        // 开始时间
        Long startTime = System.currentTimeMillis();
        // 模拟100个用户
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                // 每个线程就是一个用户
                // 模拟每个用户发起十次访问请求

                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 每个线程结束之后 threadSize -1
                    countDownLatch.countDown();
                }

            });
            thread.start();
        }
        // 保证100个线程结束之后,再执行后面的代码
        countDownLatch.await(); // threadSize==0 执行后面的代码
        Long endTime = System.currentTimeMillis();
        System.out.println("Time: " + (endTime - startTime) + ", count: " + count);
    }


    /**
     * @param expectCount 期望值 count
     * @param newCount    需要给 count 赋值的新值
     * @return 成功返回 true 失败返回 false
     */
    public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
        if (getCount() == expectCount) {
            count = newCount;
            return true;
        }
        return false;
    }

    private static int getCount() {
        return count;
    }
}

结果

image-20210329211048014

发现耗时时间变短了,而且又保证了线程安全

java中也有cas的应用

java从jdk1.5就将cas引入并且使用了,java中的Atomic系列就是使用cas实现的

例如,AtomicInteger(package java.util.concurrent.atomic;),有兴趣的了解的 点击我

3 CAS 能做什么?

可以解决多线程并发安全的问题,以前我们对一些多线程操作的代码都是使用synchronize关键字,来保证线程安全的问题;

现在我们将cas放入到多线程环境里我们看一下它是怎么解决的

我们假设有A、B两个线程同时执行一个int值value自增的代码,并且同时获取了当前的value,我们还要假设线程B比A快了那么0.00000001s,所以B先执行,线程B执行了cas操作之后,发现当前值和预期值相符,就执行了自增操作,此时这个value = value + 1;

然后A开始执行,A也执行了cas操作,但是此时value的值和它当时取到的值已经不一样了,所以此次操作失败,重新取值然后比较成功,然后将value值更新,这样两个线程进入,value值自增了两次,符合我们的预期。

4 CAS的缺点

1.CPU开销较大,长时间自旋非常消耗资源

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

2.不能保证代码块的原子性

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值