当前位置:   article > 正文

Java锁 —— CAS_java cas

java cas

一. 定义

        CAS(compare and swap)的缩写,译为比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。
        执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值;如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

二. 原理

        CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来

三. 硬件级别保证

        CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较——更新的原子性。它是非阻塞的且自身原子性,也就是说它效率更高且通过硬件保证,说明其更可靠。

        CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好

四. 代码示例

        AtomicInteger类主要利用CAS (compare and swap) + volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

  1. public class CASDemo {
  2. public static void main(String[] args) {
  3. AtomicInteger atomicInteger = new AtomicInteger(5);
  4. System.out.println(atomicInteger.get());
  5. System.out.println(atomicInteger.compareAndSet(5,2020)+"\t"+atomicInteger.get());//执行后,expect为2020
  6. System.out.println(atomicInteger.compareAndSet(5,1024)+"\t"+atomicInteger.get());
  7. }
  8. }

五. CAS底层原理分析(以AtomicInteger为例)

1. Unsafe,CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
(注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务)

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

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

AtomicInteger.getAndIncrement() -> unsafe.getAndAddInt () 

        CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。核心思想:比较要更新变量的值V和预期值E (compare) ,相等才会将V的值设为新值N (swap) 如果不相等自旋再来。

六. 自旋锁

        指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗(循环比较获取没有类似wait的阻塞),缺点是循环会消耗CPU

        通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁3秒钟,B随后进来后发现,当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。

  1. public class SpinLockDemo {
  2. AtomicReference<Thread> atomicReference = new AtomicReference();
  3. public void MyLock(){
  4. System.out.println(Thread.currentThread().getName()+" come in");
  5. while(!atomicReference.compareAndSet(null,Thread.currentThread())){
  6. }
  7. System.out.println(Thread.currentThread().getName()+"持有锁成功");
  8. }
  9. public void MyUnLock(){
  10. atomicReference.compareAndSet(Thread.currentThread(),null);
  11. System.out.println(Thread.currentThread().getName()+"释放锁成功");
  12. }
  13. public static void main(String[] args) {
  14. SpinLockDemo spinLockDemo = new SpinLockDemo();
  15. new Thread(()->{
  16. spinLockDemo.MyLock();
  17. //持有锁三秒
  18. try {
  19. TimeUnit.SECONDS.sleep(3);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. spinLockDemo.MyUnLock();
  24. },"t1").start();
  25. new Thread(()->{
  26. spinLockDemo.MyLock();
  27. spinLockDemo.MyUnLock();
  28. },"t2").start();
  29. }
  30. }

七. CAS的缺点

1. CAS会导致CPU空转问题

        可以看到getAndAddInt方法执行时,有个do while。如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。(CPU空转问题)(锁饥饿)

2. CAS会导致“ABA问题”

        CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的

        如下代码中,线程t1先将数据改为101,又改回100,线程t2仍以为数据没动过,为原来的100

  1. @Test
  2. public void abaProblem() {
  3. new Thread(() -> {
  4. atomicInteger.compareAndSet(100, 101);
  5. atomicInteger.compareAndSet(101, 100);
  6. }, "t1").start();
  7. new Thread(() -> {
  8. boolean b = atomicInteger.compareAndSet(100, 2022);
  9. System.out.println(Thread.currentThread().getName() + "是否修改成功:" + b + " 当前值为:" + atomicInteger.get());
  10. }, "t2").start();
  11. }

ABA问题解决方法

         使用AtomicStampedReference携带版本号的引用类型原子类,Stamped---version number版本号(因更新时,版本号会发生变化,所以可以根据数据和版本号同时判断数据是否发生过变化)

  1. public class ABADemo {
  2. //带版本号机制
  3. static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
  4. //解决ABA问题
  5. public static void main(String[] args) {
  6. new Thread(() -> {
  7. int stamp = atomicStampedReference.getStamp();
  8. System.out.println("t3默认版本号:" + stamp);
  9. //暂停1秒,让t4获取和t3一样的版本号
  10. try {
  11. TimeUnit.SECONDS.sleep(1);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. atomicStampedReference.compareAndSet(100, 101,stamp,stamp+1);
  16. System.out.println("t3第1次版本号: "+atomicStampedReference.getStamp());
  17. atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
  18. System.out.println("t3第2次版本号: "+atomicStampedReference.getStamp());
  19. }, "t3").start();
  20. new Thread(() -> {
  21. int stamp = atomicStampedReference.getStamp();
  22. System.out.println("t4默认版本号:"+stamp);
  23. //暂停3秒,让上面的t3完成ABA问题
  24. try {
  25. TimeUnit.SECONDS.sleep(3);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. boolean b = atomicStampedReference.compareAndSet(100, 2022,stamp,stamp+1);
  30. System.out.println(Thread.currentThread().getName() + "是否修改成功:" + b + " 当前值为:" + atomicStampedReference.getReference()+" 当前版本号:"+atomicStampedReference.getStamp());
  31. }, "t4").start();
  32. }
  33. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/311414
推荐阅读
相关标签
  

闽ICP备14008679号