• java中的CAS乐观锁


    最近,总是听到同事在面试的时候问候选人java中的锁相关的知识,大部分同学在问到CAS的时候会有些一知半解;

    1. 原子操作

    说到原子操作,会想到数据库事务中的原子性,道理都差不多,指一行或多行代码要么都执行成功或失败。
    比如:i++这行代码,在执行的过程中会分为三步去执行:

    1.取出i的值;
    2.将i的值+1;
    3.将+1后的赋值给i;

    在单线程的情况下,这种操作不会有问题,但是多线程的情况下呢:
    java中的CAS乐观锁
    出现了线程B的结果将线程A的结果覆盖的情况;那就可以说i++不是原子操作;

    可以本地验证下是不是这样的:

    private static int count = 0;
    public static void add() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for(int i=0;i<100;i++){
            new Thread(() -> {
                add();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("计算结果(Count):"+count);
    }

    我这里面运行的结果:计算结果(Count):92

    2.什么是CAS

    Conmpare And Swap,比较和交换,实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。

    上面这段话为官方解释用语,什么意思呢?
    CAS操作包括三个操作数:

    1. 内存位置 V
    2. 预期原值 A
    3. 新值 B

    如果内存位置V与预期原值A相等,则认为没有被其它线程修改过,认为是安全的,那么处理器会自动将内存位置V的值更新为新值B;
    如果内存位置V与预期原值A不相等,则处理器不做任何操作;

    3.Java中的CAS实现

    java中的CAS锁是通过Unsafe类实现,但是方法都是native;查看openJDK可以在里面找到Unsafe.cpp源码,最后调用的是:Atomic:comxchg();
    java中的CAS乐观锁

    对于cmpxchg指令,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果是多处理器,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,就省略lock前缀;

    关于lock前缀:
    1.确保对内存的读-改-写操作原子执行。
    2.禁止该指令与之前和之后的读和写指令重排序。
    3.把写缓冲区中的所有数据刷新到内存中。

    上面的示例代码通过CAS来实现:

    private static AtomicInteger count = new AtomicInteger(0);
    
    public static void add() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.incrementAndGet();
    }
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for(int i=0;i<100;i++){
            new Thread(() -> {
                add();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("计算结果(Count):"+count.get());
    }

    无论运行多少次,结果都是:100

    4.CAS中的ABA问题

    假如现有两个线程:线程1与线程2,count=1;
    线程1:将count+1;
    线程2:将count+1,count-1;
    java中的CAS乐观锁

    ABA问题,就是线程1与线程2在执行的过程中,线程2将值由之前的A改为了B又改为了A,但此时线程1以为A还是之前的值,没有其它线程改变过,则线程1也做更新;

    ABA模拟代码:

    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 让它加1
            int expectNum = count.get();
            int updateNum = expectNum + 1;
    
            boolean result = count.compareAndSet(expectNum, updateNum);
            System.out.println("操作成功/失败:"+ result+";count="+count.get());
        });
        Thread threadB = new Thread(() -> {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count.incrementAndGet(); // +1
            count.decrementAndGet(); // -1
        });
    
        threadA.start();
        threadB.start();
    }

    这里的输出结果:操作成功/失败:true;count=1

    解决ABA问题:
    产生这种问题的原因是A-B-A时,没有一个标识来标记第一个A和第三个A是不是同一个A,如果能够加个版本号A1-2B-3A,每次变量更新的时候把版本号加一,这样就可以解决这个问题;

    Java里面提供了AtomicStampedReference来解决这个问题;

    private static AtomicStampedReference<Integer> reference = new AtomicStampedReference(new Integer(0), 1);
    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            Integer expectedReference = reference.getReference();
            Integer updateReference = expectedReference + 1;
            Integer expectedStamp = reference.getStamp();
            Integer updateStamp = expectedStamp + 1;
    
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            boolean result = reference.compareAndSet(expectedReference, updateReference, expectedStamp, updateStamp);
            System.out.println("操作成功/失败:" + result + ";count=" + reference.getReference());
        });
        Thread threadB = new Thread(() -> {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // +1
            Integer expectedReference1 = reference.getReference();
            Integer expectedStamp1 = reference.getStamp();
            reference.compareAndSet(expectedReference1, expectedReference1 + 1, expectedStamp1, expectedStamp1 + 1);
    
            System.out.println("1:"+reference.getReference()+":"+reference.getStamp());
    
            // -1
            Integer expectedReference2 = reference.getReference();
            Integer expectedStamp2 = reference.getStamp();
            reference.compareAndSet(expectedReference2, expectedReference2 - 1, expectedStamp2, expectedStamp2 + 1);
            System.out.println("2:"+reference.getReference()+":"+reference.getStamp());
    
        });
    
        threadA.start();
        threadB.start();
    }
  • 相关阅读:
    【文学文娱】《屌丝逆袭》-出任CEO、迎娶白富美、走上人生巅峰
    天纵英才-阿里巴巴《马云》
    我的《大宋王朝》
    《1024 程序员节》—我喂自己袋盐
    【文学文娱】《失控》读后感
    《由河南人--首富许家印说起》
    《将博客搬至CSDN》
    【置顶】技术每天一点点--2017.09-2018.10月
    saltstack的简单搭建
    rabbitMQ基础应用
  • 原文地址:https://www.cnblogs.com/aiaitie/p/12772345.html
Copyright © 2020-2023  润新知