【多线程:cas】原子更新器 原子累加器 缓存一致性问题

简介: 【多线程:cas】原子更新器 原子累加器 缓存一致性问题

【多线程:cas】原子更新器 原子累加器 缓存一致性问题

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 + '\'' +
                '}';
    }
}

结果

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);
    }
}

结果

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;

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();  
    }  
}

解释
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读取的数据是单独的 避免对方缓存行失效的问题。解决缓存一致性的方法是典型的以空间换时间处理方式。

目录
相关文章
|
6天前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
24天前
|
消息中间件 缓存 监控
如何保证缓存和数据库的一致性?
保证缓存和数据库的一致性的做法
|
2月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
25 0
|
4月前
|
算法 安全 Java
Java多线程基础-12:详解CAS算法
CAS(Compare and Swap)算法是一种无锁同步原语,用于在多线程环境中更新内存位置的值。
46 0
|
16天前
|
消息中间件 缓存 NoSQL
奇怪的缓存一致性问题
本文记录了缓存一致性问题的排查过程和解决方案,同时带读者朋友们一起回顾下相关的八股文。
|
17天前
|
缓存 NoSQL 关系型数据库
MySQL与Redis缓存一致性的实现与挑战
在现代软件开发中,MySQL作为关系型数据库管理系统,广泛应用于数据存储;而Redis则以其高性能的内存数据结构存储特性,常被用作缓存层来提升数据访问速度。然而,当MySQL与Redis结合使用时,确保两者之间的数据一致性成为了一个重要且复杂的挑战。本文将从技术角度分享MySQL与Redis缓存一致性的实现方法及其面临的挑战。
42 2
|
24天前
|
消息中间件 缓存 监控
go-zero微服务实战系列(六、缓存一致性保证)
go-zero微服务实战系列(六、缓存一致性保证)
|
2月前
|
缓存 NoSQL 数据库
Redis问题之在高并发场景下,保证Redis缓存和数据库的一致性如何解决
Redis问题之在高并发场景下,保证Redis缓存和数据库的一致性如何解决
|
1月前
|
存储 缓存 NoSQL
基于SpringBoot+Redis解决缓存与数据库一致性、缓存穿透、缓存雪崩、缓存击穿问题
这篇文章讨论了在使用SpringBoot和Redis时如何解决缓存与数据库一致性问题、缓存穿透、缓存雪崩和缓存击穿问题,并提供了相应的解决策略和示例代码。
55 0
|
2月前
|
canal 消息中间件 缓存
面试题:如何解决缓存和数据库的一致性问题?
面试题:如何解决缓存和数据库的一致性问题?
52 1