第八章CAS策略

JDK提供的原子类

1671202811439.jpg

  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

Atomic的原理

  • Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

没有CAS之前

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

常用synchronized锁,但是它比较重 ,牵扯到了用户态和内核态的切换,效率不高。

public class CASDemo1 {
	//利用volatile
    private volatile int num;

    public int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        num++;
    }
}

引入CAS之后

public class CASDemo1 {
  
    AtomicInteger atomicInteger = new AtomicInteger();
    public int getNum() {
        return atomicInteger.get();
    }

    public void setNum(int num) {
        atomicInteger.getAndIncrement();
    }
}

CAS是什么

  • Compare and Swap比较交换,不会真正的阻塞线程,不断尝试更新,是乐观锁的一种实现方式

  • 中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。

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

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

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

CAS原理

  • CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
    • 当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
      • 当它重来重试的这种行为成为—自旋!
image-20230615183132937 image-20230615183353242 image-20230615183114477
  • 线程2操作失败,进行自旋操作,重新读取主内存中的值,进行再次重试

代码演示

public class CASDemo2 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,200)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,300)+"\t"+atomicInteger.get());
    }
}
//true	200
//false 200

如何保证的原子性

  • 那有没有可能我在判断了 线程1的A为0为之后,正准备更新它的新值的时候,被其它线程更改了 i 的值呢?
    • 不会的。因为CAS是⼀种原子操作,它是⼀种系统原语,是⼀条CPU的原⼦指令,从CPU层⾯保证它的原子性
      • 也就是说我们比较和交换这两个操作要么全部成功要么全部失败

硬件级别保证

  • 对总线加锁,效率比synchronized效率高。

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

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

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

  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作
    • 也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好

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

实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。

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

源码分析

//compareAndSet
//发现它调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//compareAndSwapInt
//发现它调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面三个方法都是类似的,主要对4个参数做一下说明。

  • var1:表示要操作的对象
  • var2:表示要操作对象中属性地址的偏移量
  • var4:表示需要修改数据的期望的值
  • var5/var6:表示需要修改为的新值

引出来一个问题:Unsafe类是什么?

CAS底层原理?如果知道,谈谈你对UnSafe的理解

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;//保证变量修改后多线程之间的可见性
}
  • 天上飞的理念,必有落地的实现

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

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

变量valueOffset

  • 表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));

变量value用volatile修饰

  • value值用volatile是为了实现可见性,能让线程能够实时获得主内存的最新值来进行比较

例子atomicInteger.getAndIncrement()为什么安全

  • CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
    • 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  • AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

image-20230703102558402

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

  • var5是表示需要修改数据的期望的值,是提高getIntVolatile()获得的,一看方法名就知道是获得主内存最新的值
  • compareAndSwapInt就是操作系统提供的原子操作CAS
    • 如果我们的期望值(刚刚获取的var5)跟主内存中值相同,则进行修改,返回true,取反就退出循环
      • 通过C语言指针类似,直接操作内存获取对应的数据,var1是this,var2是偏移地址
    • 如果我们的期望值跟主内存中的值不同,则返回false,取反就继续循环,再次尝试

自定义原子引用

  • 譬如AtomicInteger原子整型,可否有其他原子类型?比如AtomicBook、AtomicOrder*
  • 可以!
  • 丢入泛型中Class AtomicReference<V>
@Getter
@ToString
@AllArgsConstructor
class User{
    String userName;
    int age;
}
public class CASDemo3 {
    public static void main(String[] args) {
        User zhangSan = new User("zhangsan", 20);
        User liSi = new User("lisi", 22);
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(zhangSan);
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
    }
}
true	User(userName=lisi, age=22)
false	User(userName=lisi, age=22)

CAS与自旋锁

自旋锁(spinlock)

  • 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁 ,

  • 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

若在OpenJDK源码中查看Unsafe.java

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  • 这里while体现了自旋的思想

  • 假如是ture,取反false退出循环;假如是false,取反true要继续循环。

实现自旋锁

题目:实现一个自旋锁
自旋锁好处:循环比较获取没有类似wait的阻塞。

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

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"尝试获取锁");
        //只有没有线程占用的时候,才能加锁
        while (!atomicReference.compareAndSet(null,thread)){

         }
        //如果是空的,那么就吧thread放进去
        System.out.println(Thread.currentThread().getName()+"\t"+"获得了这个自旋锁");
    }
    public void unLock(){
        Thread thread = Thread.currentThread();
        //只有当前占用锁的线程才能进行解锁
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t"+"-------任务完成,解锁.....");
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        new Thread(()->{
            spinLock.lock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLock.unLock();
        },"t1").start();
        new Thread(()->{
            spinLock.lock();
            spinLock.unLock();
        },"t2").start();
    }
}
t1	尝试获取锁
t1	获得了这个自旋锁
t2	尝试获取锁
t1	-------任务完成,解锁.....
t2	获得了这个自旋锁
t2	-------任务完成,解锁.....
  • while (!atomicReference.compareAndSet(null,thread))
    • 相当于CAS(V,A,B),V表示当前锁的拥有者,也就是占用锁的线程 A(null)表示希望当前没有线程拥有这个锁 B表示当前的执行这个方法线程
      • 当ANULL的时候,表示当前自旋锁没有被任何线程拥有,尝试将this.ownerThread.currerntThread() ,将持有锁的对象变成当前线程

CAS缺点

  • ABA 问题

  • 循环时间长开销大

  • 只能保证一个共享变量的原子操作

循环时间长开销大

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

do while 如果它一直自旋会一直占用CPU时间,造成较大的开销

  • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
    • 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

ABA问题

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

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
img

解决的方法

  • 在ABA问题中引入版本号
    • 当CAS V==A的时候,才会用到版本号,来判断当前的主内存的V是否被别的线程反复修改过

JDK的实现

AtomicStampedReference版本号 (注意区分前面的Class AtomicReference<V>

AtomicStampedReference(V initialRef, int initialStamp)
创建一个新的 AtomicStampedReference与给定的初始值。

ABA问题复现

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get());
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(101, 100)+"\t"+atomicReference.get());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 103)+"\t"+atomicReference.get());
        },"t2").start();
    }
}
t1操作true	101
t1操作true	100
t2操作true	103

添加版本号

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp+"\t"+"数值为"+atomicStampedReference.getReference());//1-----------初始获得一样的版本号
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 101,1,2)+"\t"
                    +"第二次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(101, 100,2,3)+"\t"+
                    "第三次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 103,1,2)+"\t"+
                    "第四次版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t2").start();
    }
}
t1	 首次版本号:1	数值为100
t1操作	true	第二次操作版本号为2	数值为101
t1操作	true	第三次操作版本号为3	数值为100
t2操作	false	第四次操作版本号为3	数值为100

只能保证一个共享变量的原子操作

  • 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

JDK相关原子类使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里不会投三分

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值