JUC并发编程——CAS 介绍及底层源码分析

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

1、什么是 CAS

前言:使用锁的代价

Java并发处理中锁非常重要,但是使用锁会带来下面几个问题:

  • 加锁、释放锁会需要操作系统进行上下文切换和调度延时,在上下文切换的时候,cpu之前缓存的指令和数据都将失效,这个过程将增加系统开销。
  • 多个线程同时竞争锁,锁竞争机制本身需要消耗系统资源。没有获取到锁的线程会被挂起直至获取锁,在线程被挂起和恢复执行的过程中也存在很大开销。
  • 等待锁的线程会阻塞,影响实际的使用体验。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

乐观锁和悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁:总是假设最坏的情况,认为线程每次访问共享资源的时候,总会发生冲突,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。宁愿牺牲性能(时间)来保证数据安全。synchronized 关键字的实现就是悲观锁。

乐观锁:顾名思义,就是很乐观,每次线程访问共享资源不会发生冲突,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。CAS机制(Compare And Swap)就是一种乐观锁。

CAS (Compare And Swap) 比较并交换

CAS (Compare And Swap) 比较并交换是一种现代 CPU 广泛支持的CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。

CAS的功能是判断内存某个位置的值是否为预期值。如果是则更改为新的值,这个过程是原子的。CAS可以将read- modify - write这类的操作转换为原子操作。

read- modify - write类型的操作,如i++,i++自增操作包括三个子操作:

● 从主内存读取i变量值

● 对i的值加1

● 再把加1之后 的值保存到主内存

CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新。

CAS 有三个操作参数:

  1. 内存位置 V(它的值是我们想要去更新的)
  2. 预期原值 A(上一次从内存中读取的值)
  3. 新值 B(应该写入的新值)

CAS的操作过程:将内存位置V的值与A比较(compare),如果相等,则说明没有其它线程来修改过这个值,所以把内存V的的值更新成B(swap),如果不相等,说明V上的值被修改过了,不更新,而是返回当前V的值,再重新执行一次任务再继续这个过程。

2、JDK 对 CAS 的支持

JDK 提供了对CAS 操作的支持,具体在sun.misc.unsafe类中,声明如下:

public final native boolean compareAndSwapObject(Object o, long offset,
                                                 Object expected,
                                                 Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);                                               
public final native boolean compareAndSwapLong(Object o, long offset,
                                                   long expected,
                                                   long x);  
复制代码

参数说明:

  • o:表示要操作的字段对象
  • offset:字段在对象内的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值
  • expected:字段的期望值
  • x:如果该字段的值等于字段的期望值,用于更新字段的新值

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

Unsafe 类中操作内存数据的方法:

//分配内存指定大小的内存
public native long allocateMemory(long bytes);

//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);

//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);

//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);

//设置给定内存地址的值
public native void putAddress(long address, long x);

//获取指定内存地址的值
public native long getAddress(long address);

//设置给定内存地址的long值
public native void putLong(long address, long x);

//获取指定内存地址的long值
public native long getLong(long address);


//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
public native byte getByte(long address);

public native void putByte(long address, byte x);

//操作系统的内存页大小
public native int pageSize();
复制代码

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

Java中CAS的操作:

public class Demo01 {
    public static void main(String[] args) {
        //创建原子类
        AtomicInteger atomicInteger = new AtomicInteger(6);
        //一个是期望值,一个是更新值,期望值和原来的值相同时,才能够更改
        System.out.println(atomicInteger.compareAndSet(6, 7));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(6, 8));
        System.out.println(atomicInteger.get());
    }
}
复制代码

在这里插入图片描述

这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经改成了7,不满足期望值,因此返回了false,本次写入失败。

compareAndSet方法源码:

/**
如果当前值==预期值,则原子地将值设置为给定的更新值。
参数:
expect期望 - 期望值
update更新 - 新值
返回:
如果成功则为true 。 返回false表示实际值不等于预期值。
 */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
复制代码

compareAndSet底层调用了unsafe的compareAndSwapInt方法。

3、原子类操作源码分析

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的Unsafe类,并且统统线程安全。

以 AtomicInteger 原子类为例,进行源码分析:

AtomicInteger 有一个getAndIncrement方法,可以以原子方式将当前值递增 1,源码为:

public final int getAndIncrement() {
    //this 表示当前对象,valueOffset 表示偏移量
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
复制代码

追踪 valueOffset 的源码:

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        //objectFieldOffset(Field f):方法用于获取value字段相对Java对象的“起始地址”的偏移量,返回值为Long类型
        //参数 AtomicInteger.class.getDeclaredField("value"):获取本类的value字段
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
//value 字段加上volatile,保证可见性,相当于i++中的i
private volatile int value;

/**
 * 使用给定的初始值创建新的AtomicInteger。即value = initialValue;
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

/**
   创建一个初始值为0的新AtomicInteger实例。即value=0;
 */
public AtomicInteger() {
}
复制代码

关于偏移量的理解:一个java对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,同时考虑到对齐要求,可能这些字段不是连续放置的,用这个方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它其实跟某个具体对象又没什么太大关系,跟class的定义和虚拟机的内存模型的实现细节更相关。

在getAndIncrement方法的源码中,可以看见调用了unsafe的getAndAddInt方法,继续追踪getAndAddInt的源码:

/**
 * 以原子方式将给定值delta添加到给定对象o中给定偏移量offset处的字段,
 * 参数 o – 用于更新的字段
       offset偏移量 - 字段偏移量
       delta – 要添加的值,在getAndIncrement方法中,delt=1
 */
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        //通过对象和偏移量获得变量的值,并赋给v
        v = getIntVolatile(o, offset);
    /*
	compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过o和offset获取变量的值,
	如果对象 o ,内存偏移量offset处的变量值和期望值 v一样,就把偏移量offset处的值改成v + delta,
	compareAndSwapInt()返回true,退出循环,
	如果对象 o ,内存偏移量offset处的变量值和期望值 v不一样,说明其他线程修改了offset地址处的值,此时
	compareAndSwapInt()返回false, 继续循环
	Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被 
    其他线程中断,这也是一个内存操作,效率高
    这个do-while是一个标准的自旋操作,也称自旋锁,
	*/
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
复制代码

4、原子引用解决ABA问题

CAS 机制的缺点

1、CPU 开销过大

  • 在并发量比较高的时候,如果许多线程都尝试去更新一个变量的值,却又一直比较失败,导致提交失败,产生自旋,循环往复,会对CPU造成很大的压力和开销。

2、不能确保代码块的原子性(注意是代码块)

  • CAS机制所确保的是一个变量的原子性操作,而不能保证整个代码块的原子性,比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized或者lock了。

3、ABA 问题

什么是ABA问题

一个线程T1从内存位置V中取出A,想要把内存位置数据变成C,这时候另一个线程T2也从内存中取出A,线程T2速度快并且线程T2进行了一些操作将A变成了B,然后线程T2又将V位置的数据还原成A,这时候线程T1才进行CAS操作发现内存中仍然是A,然后T1操作成功。尽管线程T1的CAS操作成功,但是不代表这个过程就是没有问题的。

模拟ABA问题:

public class Demo01 {
    public static void main(String[] args) {
        //创建原子类
        AtomicInteger atomicInteger = new AtomicInteger(6);
        ////线程T2
        System.out.println(atomicInteger.compareAndSet(6, 7));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(7, 6));
        System.out.println(atomicInteger.get());

        //线程T1,线程T1并不知道内存位置数据被动过
        System.out.println(atomicInteger.compareAndSet(6, 7));
        System.out.println(atomicInteger.get());
       
    }
}
复制代码

解决ABA问题

使用原子引用类 AtomicStampedReference 解决ABA问题。

AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳或版本号(这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

AtomicStampedReference构造方法:

在这里插入图片描述

解决ABA问题代码实例:

package com.cheng.CASDemo01;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Demo01 {
    public static void main(String[] args) {
        //创建原子引用类,初始引用为6,初始版本号为1
        AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(6,1);
        //线程T2
        new Thread(()->{

            int stamp = atomicInteger.getStamp();//获得版本号
            System.out.println("第一次版本号a1->"+stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            //compareAndSet操作需要四个参数,分别是预期值,新值,预期版本号,新版本号
            System.out.println(atomicInteger.compareAndSet(6, 7,
                    atomicInteger.getStamp(), atomicInteger.getStamp() + 1));

            //获得cas操作后的版本号
            System.out.println("第二次版本号a2->"+atomicInteger.getStamp());

            //把内存地址的值改回6
            System.out.println(atomicInteger.compareAndSet(7, 6,
                    atomicInteger.getStamp(), atomicInteger.getStamp() + 1));

            //获得cas操作后的版本号
            System.out.println("第三次版本号a3->"+atomicInteger.getStamp());

        }).start();


        //线程T1
        new Thread(()->{
            int stamp = atomicInteger.getStamp();
            System.out.println("b1->"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicInteger.compareAndSet(6, 10,
                    stamp, stamp + 1));

            System.out.println("b2->"+atomicInteger.getStamp());

        }).start();
    }
}
复制代码

查看运行结果:

在这里插入图片描述

猜你喜欢

转载自juejin.im/post/7017079463670186015