今天终于彻底懂了原子类!!舒服

最近,小强公司业务最近想要做个服务监控预警的功能,其实就是统计节点的实时QPS,实时线程数,当QPS或者线程数大于一定值会报错,小强果断上了Long 类型,结果。。。。发现实际的值比统计的值大。。
导致一个很严重的线上事故,被祭天了。。。。
image.png
小强下来也很不解,因为在测试环境测试过,是没问题的。。。。。。 他自己模拟了一下,发现果真有问题

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
image.png

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

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

1.什么是原子操作

想必很多同学都用过事务,原子性是事务中的一大特性,表示一个事务包含多个操作,要么全部成功,要么全部失败。
实现原子操作可以使用锁,锁机制可以满足基本的需求了,但是synchronized关键字是基于阻塞的锁机制,对于锁的处理不够灵活。

2.如何实现原子操作

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

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

3.AtomicInteger/AtomicLong

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

1.解决监控并发的类

public class CasDemo {
    public static AtomicLong count = new AtomicLong(0L);

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

        ExecutorService executorService = Executors.newCachedThreadPool();


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

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

image.png

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

关键方法分析:

public long int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
    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的过程。循环结果如下:
image.png

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

你肯定以为到这就我们就结束了,那你大错特错了,里面有好多坑你知道么?
接着继续往下看!!!里面到底有啥坑
image.png

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. 调用者能够直接操作对象字段。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

举个栗子 !!!

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

1707044540212.png

1.架构总览

LongAdder 在高并发的场景下会比AtomicLong具有更好的并发性能
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同的线程会放到不同的槽中,各个线程对自己的槽中的值进行CAS操作,这样其实就就相当于热点分散了,冲突的概率会变小,这个其实就是和ConcurrentHashMap 中的“分段锁”其实就是类似的思路。

image.png

代码示例

public static void main(String[] args) throws Exception {
    long start1 = System.currentTimeMillis();
    testLongAdder();
    System.out.println("LongAdder 耗时:" + (System.currentTimeMillis() - start1) + "ms");
    long start2 = System.currentTimeMillis();
    testAtomicLong();
    System.out.println("AtomicLong 耗时:" + (System.currentTimeMillis() - start2) + "ms");
    System.out.println("----------------------------------------");
}

static void testAtomicLong() throws Exception{
AtomicLong atomicLong = new AtomicLong();
List<Thread> list = new ArrayList();
for (int i = 0; i < 9999; i++) {
    list.add(new Thread(() -> {
        for(int j=0;j<100000;j++){
            atomicLong.incrementAndGet();
        }
    }));
}

for (Thread thread : list) {
    thread.start();
}
for (Thread thread : list) {
    thread.join();
}

System.out.println("AtomicLong value is : " + atomicLong.get());
}

static void testLongAdder() throws Exception{
    LongAdder longAdder = new LongAdder();
    List<Thread> list = new ArrayList();
    for (int i = 0; i < 9999; i++) {
        list.add(new Thread(() -> {
            for(int j=0;j<100000;j++){
                longAdder.increment();
            }
        }));
    }

    for (Thread thread : list) {
        thread.start();
    }
    for (Thread thread : list) {
        thread.join();
    }

    System.out.println("LongAdder value is : " + longAdder.longValue());
}

2.关键方法

1.LongAdder.increment()

public void increment() {
    add(1L);
}
   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()

    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 **的原因之一。

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

image.png

  • 42
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值