当前位置:   article > 正文

JUC并发编程与源码分析笔记08-CAS

juc并发编程与源码分析

原子类

java.util.concurrent.atomic。

没有CAS之前

多线程环境不使用原子类,要想保证线程安全,就得加synchronized锁。

使用CAS之后

多线程环境使用原子类,保证i++线程安全,类似于乐观锁。

是什么

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

原理

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

demo

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        System.out.println(atomicInteger.compareAndSet(1, 123) + "-" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(1, 123) + "-" + atomicInteger.get());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

硬件级别保证

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

CAS底层原理,Unsafe的理解

  1. Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地native方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
    注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
  2. CAS方法中变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
  3. 变量value用volatile修饰,保证多线程之间内存可见性。

AtomicInteger类主要是利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。
核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

原子引用

除了AtomicInteger,我们可以自定义原子类嘛,是可以的,JDK给我们提供了一个AtomicReference,类似一个包装类。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference<Student> atomicReference = new AtomicReference<>();
        Student z3 = new Student("z3", 10);
        Student l4 = new Student("l4", 10);
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, l4) + "-" + atomicReference.get());
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Student {
    String name;
    int age;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

CAS与自旋锁,借鉴CAS思想

自旋锁(SpinLock):CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null, thread)) {
            System.out.println(thread.getName() + "进行CAS获取锁失败,重试");
        }
        System.out.println(thread.getName() + "获取到锁");
    }

    public void unLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + "释放锁");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        }, "threadA").start();
        Thread.sleep(900);
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        }, "threadB").start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

CAS缺点

循环时间太长开销很大

看CAS的源码,我们也发现了,是一个死循环,直到CAS成功,才会停止执行,如果一直没有成功,就是在空跑CPU消耗资源。

ABA问题

一个线程获取到变量的值是A,它打算把变量的值改为B,不过这时候另一个线程优先级更高,提前将A改为了B,然后又将B改为了A,原始线程在进行CAS的时候,依然是成功的,但是原始线程并不知道,变量的值已经发生过变动,这可能就是有问题的。
要想解决ABA问题,就要引入一个新的类:AtomicStampedReference。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    public static void main(String[] args) throws InterruptedException {
        Student z3 = new Student("z3", 30);
        Student l4 = new Student("l4", 40);
        Student w5 = new Student("w5", 50);
        AtomicStampedReference<Student> atomicStampedReference = new AtomicStampedReference<>(z3, 1);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "第一次获取的版本号是:" + atomicStampedReference.getStamp());
            try {
                Thread.sleep(500);// 为了让thread2第一次拿到的版本号和thread1第一次拿到的版本号一致
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(z3, l4, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);// z3改为l4
            System.out.println(Thread.currentThread().getName() + "第二次获取的版本号是:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(l4, z3, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);// l4改为z3
            System.out.println(Thread.currentThread().getName() + "第三次获取的版本号是:" + atomicStampedReference.getStamp());
        }, "thread1").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "第一次获取的版本号是:" + stamp);
            try {
                Thread.sleep(1000);// 为了让thread1发生ABA问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicStampedReference.compareAndSet(z3, w5, stamp, stamp + 1);// z3改为w5
            System.out.println(b + "\t" + atomicStampedReference.getReference() + "\t" + atomicStampedReference.getStamp());
        }, "thread2").start();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Student {
    String name;
    int age;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/966819
推荐阅读
相关标签
  

闽ICP备14008679号