四时宝库

程序员的知识宝库

「多线程基础」CAS原理深入分析(cas怎么实现的)

什么是CAS

Compare And Swap,翻译成中文就是比较并交换

  • 操作系统层面的CAS操作,是一条CPU的原子指令:cmpxchg指令,该指令具备原子性,使用CAS操作数据时不会造成数据不一致问题。
  • Java应用层面的CAS操作,是由位于sun.misc包下的Unsafe类对操作系统底层CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API,这些API就是JDK5开始,新增的JUC并发包。

CAS在Java中的应用

Java中的CAS操作,依赖于sun.misc.Unsafe类实现,该类提供native方法,直接调用操作系统的CAS指令。

关于Unsafe类的使用,包括以下方面:

获取Unsafe实例

public class JvmUtil {
    /**
     * 获取Unsafe实例对象
     * sun.misc.Unsafe类被定义成final类型,因此不允许被继承,其构造方法为private的方法
     * 因此无法在外部对Unsafe进行实例化,可以通过反射的方式,自定义获取Unsafe实例
     * 非必要不要直接用该类!!!
     * @return Unsafe实例
     */
    public static Unsafe getUnsafe() {
        try {
            //theUnsafe为sun.misc.Unsafe类中定义的私有变量
            //private static final Unsafe theUnsafe
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

调用Unsafe提供的CAS方法

/**
 * @param o 		需要操作的字段所处的对象
 * @param offset	需要操作的字段相对于对象头的偏移量
 * @param expected	期望值(旧值)
 * @param update	更新值(新值)
 * @return			true 更新成功  | false 更新失败
 */
public final native boolean compareAndSwapObject(
    Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(
    Object o, long offset, int expected, int update);
public final native boolean compareAndSwapLong(
        Object o, long offset, long expected, long update);

以上方法直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg。

在执行Unsafe的CAS方法时,这些方法首先将内存位置的值期望值(旧值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回true;如果不匹配,处理器不做任何操作,并返回false。

调用Unsafe提供的字段偏移量方法

//获取静态属性Field在Class对象中的偏移量
public native long staticFieldOffset(Field field);

//获取非静态属性Field在Class对象中的偏移量,相对于Object对象头部的偏移量,相对的内存地址
public native long objectFieldOffset(Field field);

字段偏移量计算时,首先确定需要计算的字段是什么属性的,是静态的还是非静态的。若是静态的成员,则属于类的成员而不是对象的成员。

CAS的优势主要有两点:

  • CAS属于无锁编程,线程之间不存在阻塞和唤醒这些重量级的操作;
  • 进程不存在用户态和内核态之间的运行切换,进程不需要承担频繁切换带来的开销。

尽管使用CAS进行无锁编程具备以上优势,但是仍然存在弊端。

CAS的弊端及规避措施

1、ABA问题

使用CAS操作内存数据时,当数据发生过变化也能更新成功,如在操作序列A-->B-->A时,最后一个元素CAS的预期值A实际已经发生过更改,但也能更新成功,这就产生了ABA问题。

ABA问题的解决思路就是使用版本号。在变量前面或后面追加上版本号,每次更新变量时将版本号加1,那么操作序列A-->B-->A就变成了A1-->B2-->A3,如果将A1当做A3的期望值,就会操作失败。

JDK提供了AtomicStampedReference和AtomicMarkableReference两个类来解决ABA问题。比较常用的是AtomicStampedReference类,该类的compareAndSet方法会首先检查当前引用是否等于预期引用,再检查当前印戳是否等于预期印戳,如果全部相等,则以原子方式将引用和印戳设置为新值。

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

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

一个比较简单的规避方法,把多个共享变量合并成一个共享变量来操作。

JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在AtomicReference实例后,再进行CAS操作。例如有两个共享变量i=1、j=2,可以将二者合并成一个对象,然后用CAS来操作合并对象的AtomicReference引用。

3、无效CAS会带来额外开销

自旋CAS如果长时间不成功,会给CPU带来极大的开销,从而降低程序的性能。可以使用限制自旋次数的方案解决。

4、部分CPU平台上存在“总线风暴”问题

CAS操作和volatile一样,也需要CPU通过MESI协议保证各个内核的“Cache一致性”。CPU通过BUS总线发送大量的MESI协议相关的信息,产生“Cache一致性流量”。因为总线的“通信能力”被设计成固定的,如果Cache一致性流量过大,总线将成为瓶颈,这就是所谓的“总线风暴”。

CAS在JDK中的广泛应用

  • JUC的atomic包中的原子类:使用CAS保障对数字成员操作的原子性。
  • Java AQS:通过CAS保障其内部双向队列头部、尾部操作的原子性
  • 显示锁、CurrentHashMap:基于AQS、JUC.atomic包中的原子类实现。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接