菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
190
0

CAS的理解

原创
05/13 14:22
阅读数 47502

CAS(CompareAndSweep)工作方式

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。   

​ CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

过程图解如下:

  1. 内存地址V当中,存储着值为10的变量

  1. 此时线程1想要把变量的值增加1,对线程1来说,旧的预期值A=10,要修改的新值B=11

  1. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

  1. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

  1. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

  1. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

  1. 线程1进行SWAP,把地址V的值替换为B,也就是12。


CAS的特点

结合CAS和volatile可以实现无锁并发,适用于线程数少,多核CPU场景下

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我再重试呗
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
  • CAS体现的是无锁并发,无阻塞并发
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,重试必然会频繁发生,反而效率会受影响

原子整数

JUC并发包提供了

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以AtomicInteger为例

public static void main(String[] args){

        AtomicInteger i = new AtomicInteger(3);

        System.out.println(i.incrementAndGet()); // ++i

        System.out.println(i.getAndIncrement()); // i++

        System.out.println(i.addAndGet(12)); //先加后取值

        System.out.println(i.getAndAdd(12)); //先取值后加

        System.out.println(i.get());

        //                              读取到    设置值
        System.out.println(i.updateAndGet(x -> x * 10));

    }

原子引用ABA问题

public class getClassTest {
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) {

        System.out.println("main start...");
        String prev = ref.get();
        
        other();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": A -> C " + ref.compareAndSet("A", "C"));
    }

    public static void other() {
        
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": A -> B " + ref.compareAndSet("A", "B"));
        }, "t1").start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": B -> A " + ref.compareAndSet("B", "A"));
        }, "t2").start();
    }
}

主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,如果主线程希望:

只要有其他线程 动过了 共享变量,那么自己的cas就算失败,这时,仅比较值是不够的,需要再加一个版本号

public class getClassTest {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) {

        System.out.println("main start...");
        //获取值A
        String prev = ref.getReference();
        //获取版本号
        int stamp = ref.getStamp();
        
        System.out.println(Thread.currentThread().getName() + " stamp : " + stamp);
        
        other();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(Thread.currentThread().getName() +" stamp : "+ stamp);

        System.out.println(Thread.currentThread().getName() + ": A -> C " + ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    public static void other() {

        new Thread(() -> {
            int stamp = ref.getStamp();
            
            System.out.println(Thread.currentThread().getName() + " stamp : " + stamp);
            
            System.out.println(Thread.currentThread().getName() + ": A -> B " + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
        }, "t1").start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            int stamp = ref.getStamp();
            
            System.out.println(Thread.currentThread().getName() + " stamp : " + stamp);

            System.out.println(Thread.currentThread().getName() + ": B -> A " + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
        }, "t2").start();
    }
}

发表评论

0/200
190 点赞
0 评论
收藏