• 【面试:并发篇33:cas】原子更新器 原子累加器 缓存一致性问题


    【面试:并发篇33:cas】原子更新器 原子累加器 缓存一致性问题

    00.前言

    如果有任何问题请指出,感谢。

    01.原子更新器

    介绍

    原子更新器又叫字段更新器,作用是成员变量更新时保证原子性
    AtomicReferenceFieldUp:成员变量为引用类型时
    AtomicIntegerFiledUpdater:成员变量是整型
    AtomicLongFiledUpdater:成员变量是长整型
    这里拿AtomicReferenceFieldUp举例

    AtomicReferenceFieldUp

    @Slf4j(topic = "c.Test40")
    public class Test40 {
    
        public static void main(String[] args) {
            Student stu = new Student();
    
            AtomicReferenceFieldUpdater updater =
                    AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
    
            System.out.println(updater.compareAndSet(stu, null, "张三"));
            System.out.println(stu);
        }
    }
    
    class Student {
        volatile String name;
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    结果

    true
    Student{name=‘张三’}

    解释
    可以看出cas操作成功,并且打印结果也说明更新成功,不过要注意一点,因为cas操作要求必须是可见的 所以成员变量必须用volatile修饰

    02.原子累加器:LongAdder

    介绍

    累加器顾名思义就是累加的,不过可能有同学回问之前不是已经有getAndIncrement()这个方法了吗?为什么还需要专门的累加器,原因很简单就是这个原子累加器效率更高。

    代码

    public class Test41 {
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                demo(
                        () -> new AtomicLong(0),
                        (adder) -> adder.getAndIncrement()
                );
            }
            System.out.println("s");
            for (int i = 0; i < 5; i++) {
                demo(
                        () -> new LongAdder(),
                        adder -> adder.increment()
                );
            }
        }
    
        /*
        () -> 结果    提供累加器对象
        (参数) ->     执行累加操作
         */
        private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
            T adder = adderSupplier.get();
            List<Thread> ts = new ArrayList<>();
            // 4 个线程,每人累加 50 万
            for (int i = 0; i < 4; i++) {
                ts.add(new Thread(() -> {
                    for (int j = 0; j < 500000; j++) {
                        action.accept(adder);
                    }
                }));
            }
            long start = System.nanoTime();
            ts.forEach(t -> t.start());
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            long end = System.nanoTime();
            System.out.println(adder + " cost:" + (end - start) / 1000_000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    结果

    2000000 cost:42
    2000000 cost:27
    2000000 cost:34
    2000000 cost:27
    2000000 cost:35
    s
    2000000 cost:16
    2000000 cost:13
    2000000 cost:6
    2000000 cost:6
    2000000 cost:6

    解释
    可以明显看出原子累加器效率要高很多。性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

    03.LongAdder源码

    LongAdder类有几个关键域

    	// 累加单元数组,懒惰初始化
    	transient volatile Cell[] cells;
    
    	// 基础值,如果没有竞争,则用cas累加这个域
    	transient volatile long base;
    
    	// 在cells创建或者扩容时 置为1 表示加锁
    	transient volatile int cellBusy;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    cas锁:cellBusy实现

    @Slf4j(topic = "c.Test42")  
    public class LockCas {  
        // 0 没加锁  
        // 1 加锁  
        private AtomicInteger state = new AtomicInteger(0);  
      
        public void lock() {  
            while (true) {  
                if (state.compareAndSet(0, 1)) {  
                    break;  
                }  
            }  
        }  
      
        public void unlock() {  
            log.debug("unlock...");  
            state.set(0);  
        }  
      
        public static void main(String[] args) {  
            LockCas lock = new LockCas();  
            new Thread(() -> {  
                log.debug("begin...");  
                lock.lock();  
                try {  
                    log.debug("lock...");  
                    sleep(1);  
                } finally {  
                    lock.unlock();  
                }  
            }).start();  
      
            new Thread(() -> {  
                log.debug("begin...");  
                lock.lock();  
                try {  
                    log.debug("lock...");  
                } finally {  
                    lock.unlock();  
                }  
            }).start();  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    解释
    state就相当于我们的cellBusy,加锁时用cas把0变为1 之后其他线程再想执行这段代码发现cas比较值不同 cas失败相当于获取锁失败,之后解锁时此线程调用set方法把state重新置为0 实现解锁

    缓存一致性:Cell类源码


    Cell类的源码很容易理解,不过它的注解引起了我们的 @sun.misc.Contended,这个注解是为了防止缓存行伪共享 也就是缓存一致性

    缓存一致性


    我们之前了解过java内存模型(JMM)和上面的图十分的相似 把cpu换位线程 把缓存换位工作内存就基本一样了,其实JMM并不是真实存在的它是jvm层面上对操作系统内存模型的模拟,上面的图片就是我们电脑里的真实内存形式。
    cpu可以直接从内存中获取数据,但是速度相对缓存而言慢了好几倍,所以通常我们的处理形式就是把内存中的数据拷贝到缓存中去,然后cpu从缓存中获取数据,但是这样就会出现一个问题 那就是缓存一致性。
    什么是缓存一致性

    我们来看这张图,假如我们现在有一个Cell数组,cell[0] cell[1],我们知道数组是在内存中是挨着的,而我们的缓存以缓存行的形式存放这 一个缓存行对应一块内存(64byte) 而我们现在的这个Cell数组 占48byte(一个cell占24byte),我们把数组从内存中拷贝到缓存中时 并不是只拷贝你需要的那个数据 而是拷贝一整个缓存行的数据 避免了多次拷贝数据导致性能降低,也就是cpu1会拷贝这个数组 cpu2也会拷贝这个数组,cpu1负责改变cell[0]的数据,cpu2赋值改变cell[1]的数据,但是最终同步数据到内存只会有一个成功 也就是势必有一个cpu的缓存中的数组失效,导致只改变了一个数据 假如只改变了cell[0]的数据 那么cpu肯定就需要再次拷贝这整个数组修改cell[1],这样相当于一个数组我们修改了两次才成功 如果数组元素多了呢 性能就会大大降低,这个就是缓存一致性问题。
    如何解决缓存一致性问题
    解决方法很简单就是上述提到的 @sun.misc.Contended 这个注解,这个注解的作用是在此注解对象或字段的前后各增加128byte 也就是前后各占一个缓存行的大小,这样的好处是 保证数据绝对不在一个缓存行内,使得 cpu读取的数据是单独的 避免对方缓存行失效的问题。解决缓存一致性的方法是典型的以空间换时间处理方式。

  • 相关阅读:
    egg框架使用(一)
    笔试选择题-图
    Pytorch房价预测
    一文详解Gorm
    CVPR2024|AIGC(图像生成,视频生成等)相关论文汇总(附论文链接/开源代码/解析)【持续更新】
    门阀-bitlocker
    Docker consul的容器服务更新与发现
    数组与list的转化分析
    idea中dataBase模板生成
    ARM指令集--数据处理指令
  • 原文地址:https://blog.csdn.net/m0_71229547/article/details/126062174