(二)今天终于彻底懂了CAS

最近,小强公司业务最近想要做个服务监控预警的功能,其实就是统计节点的实时QPS,实时线程数,当QPS或者线程数大于一定值会报错,小强果断上了Long 类型,结果。。。。发现实际的值比统计的值大。。

导致一个很严重的线上事故,被祭天了。。。。

小强下来也很不解,因为在测试环境测试过,是没问题的。。。。。。 他自己模拟了一下,发现果真有问题

java 复制代码
public class CasDemo {
    public static Long count = 0L;

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newCachedThreadPool();


        for (int i = 0; i <= 1000; i++) {
            executorService.execute(() -> {
                count = count + 1 ;
            });
        }

        Thread.sleep(1000);
        System.out.println(count);
        executorService.shutdown();
    }
}

按正常是应该1000,当时只有90

小强气的跟我讲,那我给他去说下这种情况到底该怎么解决!

现在看下问题到底出在哪!!! 带给大家一个新名字:原子操作

1.什么是原子操作

想必很多同学都用过事务,原子性是事务中的一大特性,表示一个事务包含多个操作,要么全部成功,要么全部失败。

实现原子操作可以使用锁,锁机制可以满足基本的需求了,但是synchronized关键字是基于阻塞的锁机制,对于锁的处理不够灵活。

2.如何实现原子操作

原子操作的基本顺序:

如何这个地址上的值和期望的值相等,则给于赋予新的值,否则不做任何事,但是要返回原值是多少。

是不是感觉很神奇,其实小强之前遇到的问题其实就是每个线程从主内存获取副本变量,并且自增,再次回写到主变量,那么假如有两个线程,同时拿到,并且同时增加1,同时写回主内存就会有相互覆盖的问题,所以最后增加的值肯定是必实际量要小。

3.AtomicInteger/AtomicLong

  • int addAndGet(int delta):将输入的数值与原子类中的值以原子性的方式相加,并返回结果。
  • boolean compareAndSet(int expect ,int update):如果输入的数值等于预 期值,则以原子方式将该值设置为输入的值,如果设置成功则返回true,否则返回false。
  • int getAndIncrement():以原子方式将当前值加 1,注意,这里返回的是自增前的值。

1.解决监控并发的类

java 复制代码
public static AtomicLong count = new AtomicLong(0L);

public static void main(String[] args) throws InterruptedException {

    ExecutorService executorService = Executors.newCachedThreadPool();


    for (int i = 1; i <= 1000; i++) {
        executorService.execute(() -> {
            count.getAndIncrement();
        });
    }

    Thread.sleep(1000);
    System.out.println(count.get());
    executorService.shutdown();
}

有没有想过上面的结果为什么是1000??? 那让我们来分析一下!!!

关键方法分析:

java 复制代码
public long int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
java 复制代码
    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

getLongVolatile()和compareAndSwapLong() 都是原子操作,其实上面的是一个循环不断CAS的过程。循环结果如下:

其实原子类比我们普通的累加多个一个步骤就是在每次真正更新内存之前去比较内存中的值是否和当初读取的值一样。

你肯定以为到这就我们就结束了,那你大错特错了,里面有好多坑你知道么?

接着继续往下看!!!里面到底有啥坑

2.cas自旋锁的三大问题

1.ABA问题

因为 CAS 需要在操作值的时候, 检查值有没有发生变化, 如果没有发生变化 则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行 检查时会发现它的值没有发生变化,但是实际上却变化了,这样就会导致ABA的问题

如何避免CAS问题,就是就是使用版本号,在变量前面追加上版本号,每次更新的时候把当前的版本加1。

2.CPU消耗大

自旋CAS如果长时间不成功,对CPU会有非常大的执行的开销,因此比较适合并发量不大的场景。

3.只能保证单共享变量的原子操作

当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操 作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候 就可以用锁。

还有一个取巧的办法, 就是把多个共享变量合并成一个共享变量来操作。比 如,有两个共享变量 i =2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始, JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把 多个变量放在一个对象里来进行 CAS 操作。

为了解决这些问题,在听我多比比几句,接着往下走起!!!哈哈哈哈哈哈

4.并发相关类

1.引用类型更新

1.AtomicReference

原子更新引用类型,可以原子性的更新多个变量,但是会有ABA问题。

2.AtomicStampedReference

利用版本戳的形式记录每次改变以后的版本号,这样的话就不会存在ABA问题。

2.类字段原子更新

前面我们所讲的几个原子更新引用类型如:AtomicReference,用于整个对象的更新。但不是每次都必须更新整个对象,有可能我们只需对对象中的某个字段进行原子性修改时,那么就需要使用原子更新字段类

  1. AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  2. AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  3. AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据

这几个类,我们重点看下AtomicIntegerFieldUpdater

  1. 字段必须是 volatile 类型的
  2. 字段不能用static修饰
  3. 字段不能用final修饰
  4. AtomicIntegerFieldUpdate 和 AtomicLongFieldUpdate 只能修改 int/long 类型的字段,不能修改包装类型。如果要修改包装类型就需要使用 AtomicReferenceFieldUpdate
  5. 调用者能够直接操作对象字段。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

举个栗子 !!!


java 复制代码
public class AtomicIntegerFieldDemo {

    private static AtomicIntegerFieldUpdater<User> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "id");

    public static void main(String[] args) {
        User user = new User(1, "xiaoqiang");

        System.out.println("调用atom.addAndGet(user, 50)方法返回值为:" + atomicIntegerFieldUpdater.addAndGet(user, 50));
        System.out.println(user);
    }
}

class User {
     volatile int id;

     String name;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + ''' +
                '}';
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

别急别急!还有最后一个点,如果你在高并发的情况下,AtomicLong增加的值的时候,会出现N个线程同时进行自旋操作,会出现大量失败不断自旋的情况,此时 AtomicLong 的自旋会成为瓶颈。来来来 !!!加个餐

5.LongAdder

1.架构总览

LongAdder 在高并发的场景下会比AtomicLong具有更好的并发性能

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同的线程会放到不同的槽中,各个线程对自己的槽中的值进行CAS操作,这样其实就就相当于热点分散了,冲突的概率会变小,这个其实就是和ConcurrentHashMap 中的"分段锁"其实就是类似的思路。

2.关键方法

1.LongAdder.increment()

java 复制代码
public void increment() {
    add(1L);
}
java 复制代码
   public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
    	//判断base变量是否更新成功
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                //取对应的槽
                (a = as[getProbe() & m]) == null ||
                //给对应的槽累加值
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

2.LongAdder.sum()

java 复制代码
    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            //base的值加上cell的值
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

LongAdder 对最终结果的求和,并没有使用全局锁,所以返回的值并不是绝对准确的,因为还会有其他线程进行计数累加,所以只能得到近似值,这也就是 LongAdder 并不能完全替代 LongAtomic 的原因之一。

3.应用举例

卖个关子:"小强呀,我们在sentinel做QPS统计就是用这个类,以后如果你要自己写中间件可能会用上,但是,现在讲的话,会陷入到讲sentinel源码中"

小强:"海哥,我拿小本本给你记上了,别忘了"

至此,我们的CAS先告一短路,掌握到这个程度基本上在工作中没问题了。

相关推荐
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹1 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田1 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术2 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱2 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19802 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi2 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇3 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生4 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料4 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理