6.CAS

1、原子类

java.util.concurrent.atomic

2、没有CAS之前

多线程环境不使用原子类保证线程安全i++(基本数据类型)

image-20220923174134290

3、使用CAS之后

多线程环境 使用原子类保证线程安全i++(基本数据类型)

类似我们的乐观锁

image-20220923174350467

4、是什么

4.1、说明

compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。 利用了CPU指令保证了操作的原子性

它包含三个操作数——内存位置、预期原值及更新值。

执行CAS操作的时候,将内存位置的值与预期原值比较:

如果相匹配,那么处理器会自动将该位置值更新为新值,

如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有

比较并交换,即是通过期望值当前值进行比较,如果比较两者相同,则修改主内存中的值,否则不修改,直至修改成功。

当我们多线程进行 自增操作时,会发生数据覆盖,导致得到的结果不正确,可以采用如下两种方式来解决:

  • 使用原子操作类AtomicInteger来进行自增(推荐),其底层就是使用rt.jar中的 Unsafe 类和 CAS 自旋。
  • 使用synchronized 不推荐)

既然用锁或synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用synchronized关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了CPU层面的指令,所以性能很高。

4.1.1、原理

CAS (CompareAndSwap)

CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B

当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来

当它重来重试的这种行为成为----自旋!!

image-20220923174833968

4.2、硬件级别保证

CASJDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令〈cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,

也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程

情况下性能会比较好。

4.3、源码分析

image-20220923175927949

image-20220923180028863

引出:UnSafe类是什么?

5、UnSafe

  • 1 Unsafe

CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。 Unsafe类存在于sun.misc包中 ,其内部方法操作可以像C的指针一样直接操作内存,因为JavaCAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  • 2 变量valueOffset,表示该变量值在内存中的偏移地址,因为 Unsafe就是根据内存偏移地址获取数据的

image-20220714214313172

image-20220714215536964

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指

令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由

若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条

CPU的原子指令,不会造成所谓的数据不一致问题。


image-20220923205416572

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  1. AtomicInteger里面的value原始值为3,即主内存中Atomiclntegervalue3,根据JMM模型,线程A和线程B各自持有一份值为3value的副本分别到各自的工作内存。

  2. 线程A通过getIntVolatile(var1, var2)拿到value3,这时线程A被挂起。

  3. 线程B也通过getlntVolatile(var1, var2)方法获取到value3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK

  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致
    说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

  5. 线程A重新获取value值,因为变量valuevolatile修饰,所以其它线程对它的修改,线程A总是能够看到,
    线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

image-20220923205910396

汇编里面加锁,CPU触发

image-20220923210546260

  • 3 变量value用volatile修饰,保证了多线程之间的内存可见性。

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。

这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,

并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条

CPU的原子指令,不会造成所谓的数据不一致问题。

6、总结

你只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性实现方式是基于硬件平台的汇编

指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令

核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

7、原子引用

7.1、AtomicInteger原子整型,可否有其他原子类型

AtomicBook

AtomicOrder

7.2、AtomicReference

@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
    String userName;
    int age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User z3 = new User("z3", 22);
        User li4 = new User("li4", 28);
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());
    }
}

image-20220923213342915


8、CAS与自旋锁,借鉴CAS思想

8.1、手写

/**
 * 题目:实现一个自旋锁,复习CAS思想
 * 自旋锁好处:循环比较获取没有类似wait的阻塞。
 * 通过CAS操作完成自旋锁,A线程先进来调月myLock方法自己持有锁5秒钟,
 * B随后进来后发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
 */
public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        //B进来一直循环,直到a释放锁
        while (!atomicReference.compareAndSet(null, thread)) {

        }
        System.out.println(Thread.currentThread().getName() + "\t" + "---come in");

    }

    public void unLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t ----task over, unLock");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()->{
            spinLockDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        },"a").start();
        //线程a先于b
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            spinLockDemo.lock();

            spinLockDemo.unLock();
        },"b").start();

    }
}

image-20221020184514412

image-20220923214637224

9、CAS缺点

CAS 自旋锁和 synchronized锁优缺点:

  • CAS

并发性更好,没有限制只有一个线程执行加锁的操作
CAS不断循环占用CPU较多的时间
无法解决 ABA 问题

  • synchronized

并发性较差,为了保证JMM规范的有序性,只允许一个线程执行加锁的操作。
不会占用太多CPU的时间

  • ABA问题
    概述:

一个值从A变成B,然后再变成A,此时两个A虽然数值一样,但是从实质来说,两者不是同一个A。在自旋的过程中,会将ABA中的后一个A当做前一个A。这个问题在一些情况是没问题的,在一些情况是会造成问题的。

​ 问题:

导致CAS不一定是原子性的
解决:

添加版本号,类似于乐观锁的版本号。

9.1、循环时间开销很大

image-20220923215037596

9.2、引出来ABA问题

9.2.1、ABA问题怎么产生的

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B

然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。

尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。

9.2.2、解决(版本号时间戳原子引用)

9.2.2.1、AtomicStampedReference
9.2.2.2、ABADemo
@Data
@NoArgsConstructor
@AllArgsConstructor
class Book {
    private int id;
    private String bookName;
}

public class AtomicStampedDemo {
    public static void main(String[] args) {
        Book javaBook = new Book(1, "javaBook");
        AtomicStampedReference<Book> atomicStampedReference = new AtomicStampedReference<>(javaBook, 1);
        System.out.println(atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp());//Book(id=1, bookName=javaBook)   1
        Book mysqlBook = new Book(1, "mysqlBook");
        boolean b;

        b = atomicStampedReference.compareAndSet(javaBook, mysqlBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(b + "\t" + atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp());//true Book(id=1, bookName=mysqlBook) 2

        b = atomicStampedReference.compareAndSet(mysqlBook, javaBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(b + "\t" + atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp());//true Book(id=1, bookName=javaBook)  3
    }
}

image-20220923223023789


public class ABADemo {
    
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
            //保证后面t4线程初始化拿到的版本号拿到的跟我一样
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t" + "2次版本号: " + stampedReference.getStamp());

            stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t" + "3次版本号: " + stampedReference.getStamp());

        }, "t3").start();
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
            //等待上面的t3线程,发生了ABA问题
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = stampedReference.compareAndSet(100, 2022, stamp, stamp + 1);
            System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());

        }, "t4").start();
    }
}

image-20220923224519860

9.2.3、一句话

比较+版本号一块上

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值