JUC原子类: CAS, Unsafe和原子类详解

JUC原子类: CAS, Unsafe和原子类详解

CAS

CAS的全称是Compare-And-Swap,中文含义就是对比并交换。CAS并不是java中的方法,他实际上是CPU的原子指令,==其作用是让CPU先比较两个值是否相等,然后根据判断条件原子性的更新某个位置的值。==CAS是基于硬件平台(Intel、Linux)的汇编指令,也就是说CAS是靠硬件来实现的。而在JVM或者JDK源码中,只是封装了汇编调用方法。比如AtomicInteger类便是使用了这些借口来完成多线程的安全性操作。

CAS:输入两个值,一个新值一个旧值,在CAS执行操作的时候,先比较旧值是否发生了变换,如果没有改变,则将新值赋给旧值。如果有变化,则不交换。

在保证多线程安全的时候,很多时候大家更愿意使用Synchorinized关键字,但是这种重量级锁属于排他锁,一个时刻只能有一个线程获取锁,效率较低。

CAS是CPU级别或者硬件级别的原子性操作,所以多线程使用CAS来更新数据的时候,可以不使用锁。

CAS使用实例

不使用CAS,使用Synchonized关键字或者lock。

public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}

但使用CAS

public class Test {
    private  AtomicInteger i = new AtomicInteger(0);
    public int add(){
        return i.addAndGet(1);
    }
}

这里不要着急,我们声明整型变量i不再使用int,而是使用AtomicInteger原子类,这个原子类底层是使用CAS来进行数据更新的。我们在使用的时候,对于程序员来说是不需要显示加锁的,但在多线程的环境下也能保证数据安全性。

CAS存在的问题及如何解决

问题1:ABA

因为CAS在操作的时候,需要先检查旧值是否发生了变换,如果没有变换才进行新值的更新,但是假如一个值从A变成B,又变成A,那么对于CAS来说是检测不到他的变换的,但是CAS却进行了新值的替换。

JDK1.5之后的解决方案

不在单单比较变量值的变化,而是在变量之前添加版本号进行标记,CAS再检查的时候,需要确保版本号和旧值都保持不变,则可以进行新值的交换。

落实到源代码上,JDK中的Atomic包里面的一个类可以解决ABA问题AtomicStampedReference。我们具体来看一下源代码:java.util.concurrent.atomic.AtomicStampedReference

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

主要涉及的代码如上所示。

解释:

final T reference;
final int stamp;

reference:对象引用

stamp:标志版本

private Pair(T reference, int stamp) {
    this.reference = reference;
    this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
    return new Pair<T>(reference, stamp);
}

一个构造方法,一个实例化方法。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
        // 获取当前的(元素值,版本号)对
        Pair<V> current = pair;
        return
            // 引用没变
            expectedReference == current.reference &&
            // 版本号没变
            expectedStamp == current.stamp &&
            // 新引用等于旧引用
            ((newReference == current.reference &&
            // 新版本号等于旧版本号
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }
  • 如果当前对象引用和版本标志和旧值相比没有变化,并且新值和当前值一致,则直接返回为true。
  • 如果当前对象引用和版本标志和旧值相比没有变换,但是新值和当前值并不一致,这是需要实例化一个新的pair对象,在调用CAS来实现。
  • 这块有一个比较有趣的东西,逻辑运算符
    • A&&B 如果A为true,则B执行,否则B不执行
    • A||B 如果A为false,则B执行,否则B不执行

所以在使用这个方法的时候,用户只需要传入新值和新变量的引用。是两个变量,并不是直接传入pair。

实战:
package com.wangye.Test;

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAsolved {
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,0);

    public static void main(String[] args) {
//        System.out.println(atomicStampedReference.getReference());
        Thread main = new Thread(()->{
            System.out.println("操作线程"+Thread.currentThread().getName()+",初始值 a="+atomicStampedReference.getReference());
            int stamp = atomicStampedReference.getStamp();
            try{
                Thread.sleep(1000);
            }
            catch (Exception e){
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedReference.compareAndSet(1,2,stamp,stamp+1);
            System.out.println("操作线程"+Thread.currentThread().getName()+",CAS操作结果"+isCASSuccess);
        },"主线程");

        Thread other = new Thread(()->{
            Thread.yield();
            atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("操作线程"+Thread.currentThread().getName()+"增加"+atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(2,1,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("操作线程"+Thread.currentThread().getName()+"减少"+atomicStampedReference.getReference());
        },"其他线程");

        main.start();
        other.start();
    }

}
  • 当第二个线程将stamp更改后,主线程将无法使用CAS,因为版本标志不对应了。
问题2:循环时间长

自旋问题,不是很理解。

问题3:只能保证一个共享变量的原子操作

CAS只能保证一个共享变量的原子性操作,进而保证线程安全问题。当多个共享变量的时候,CAS失效,这个时候可以用锁。或者是将所有变量合并成一个总变量,然后CAS操作这个总变量,但是比较繁琐。

从JDK1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里进行CAS操作。

JAVA中的原子类

java中的原子类都在java.util.concurrent.atomic包下。并且这些原子类基本都是由Unsafe实现的。此外,Unsafe在JUC的CAS操作中也有着广泛的应用,Unsafe中也定义了CAS的底层源码。

所以在总结原子类之前,先总结Unsafe类。

Unsafe类的功能图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCRyThYh-1599832058257)(https://www.pdai.tech/_images/thread/java-thread-x-atomicinteger-unsafe.png)]

Unsafe与CAS
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;
    }

    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

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

        return var5;
    }

    public final long getAndSetLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var4));

        return var6;
    }

    public final Object getAndSetObject(Object var1, long var2, Object var4) {
        Object var5;
        do {
            var5 = this.getObjectVolatile(var1, var2);
        } while(!this.compareAndSwapObject(var1, var2, var5, var4));

        return var5;
    }

可以看到,Unsafe中的这些方法在更新设置值的时候,都是通过不断的自旋完成的。其实自旋也就是while循环中不断执行CAS操作,知道CAS操作成功,返回True。

我们来看一下Unsafe中的原子方法

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);

这些方法都是native修饰,表示为本地方法库中的方法,是C语言编写的函数。这些方法都有一个共同的前缀compareAndSwap。我们来看看unsafe.cpp中是怎么定义的:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

我们可以看到,当Unsafe实现CAS的时候,调用了Atomic的cmpxchg方法,注意这里的Atomic并不是jdk中的Atomic,而是native的本地方法库中的c代码。cmpxchg的实现与平台有关,但是基本原理不变。

Linux

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

windows

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判断是否是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

我们可以看到,无论哪个平台,首先都是要判断是否是多处理器模型,因为只有多处理器模式的情况下,才是用内存屏障完成加锁,单处理器不需要。如果是多处理器,则将加锁。也就是为cmpxchg指令添加lock前缀。这个lock前缀有人说是总线锁,也有人说是缓存锁。这里就不区分了,总之加了锁之后,只有一个线程将被执行,其他线程阻塞,从而保证线程安全性。

Unsafe还有一些其他工能,比如修改变量的内存地址等,这些功能过于底层,不做介绍。

使用原子方式更新基本类型
  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
AtomicInteger
常用的API:
public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

其声明的变量可自动实现线程安全,而不需要加锁。

AtomicInteger的底层实现:
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            //用于获取value字段相对当前对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //返回当前值
    public final int get() {
        return value;
    }

    //递增加detla
    public final int getAndAdd(int delta) {
        //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //递增加1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

好,我们可以看到,value字段在此处被valotile修饰,表示一旦这个变量发生改变,则立即对所有的线程可见。

除此之外,可以看到,AtomicInteger中的一些方法,内部都是调用了Unsafe中相应的方法,也就是使用CAS的方式更新value。从而保证操作的原子性。

所以java.util.concurrent.atomic中的原子类,都是使用

  • valotile关键字保证变量的可见性
  • CAS保证操作的原子性

来实现的。

还有其他的原子类,这边不做介绍了,基本原理都差不多。


从面试的角度出发

  • 线程安全的实现方法

    • 互斥同步:
      • synchronized
      • ReentrantLock
    • 非阻塞同步
      • CAS
      • Atomic*原子类 (实际上还是CAS)
    • 无同步方案
      • 栈封闭
        • 方法体内的局部变量是线程私有的,不会出现多线程安全问题
        • ThreadLocal
  • 什么是CAS?

    • CAS全称可以理解为Compare and Swap,中文可以理解为比较并交换。CAS并不是java中的方法,他的实现底层是c语言。所以CAS描述的是CPU原子指令,CAS通过比较两个值是否相等,根据判断条件来完成原子性的数值更新操作。平时我们所说得CAS机制,指的是Unsafe类下的CompareAndSwap*方法,其底层实现跟具体的硬件平台有关,比如windows和Linux的实现略有不同,但是其基本原理都是通过添加锁的方式,保证多线程多处理器的安全性问题。所以说,CAS并不是JVM或者JDK中定义的方法,JVM或者JDK中只是保存了CAS的汇编调用。
  • CAS使用示例,结合AtomicInteger给出示例?

    • AtomicInteger的数据更新操作方法内调用的是Unsafe类的方法,而在Unsafe方法中调用了相应的CAS方法。
    • 示例的话比较简单,int声明的变量(不包括方法体内的局部变量)不具备多线程安全性,除非加锁;但是AtomicInteger声明的变量由于其内部的CAS机制保持原子性,所以其符合线程安全性,不需要加锁。
  • CAS会有哪些问题?

    • ABA问题:

      • 初始变量值为A,当前线程改为B,另外一个线程改为A,CAS认为变量没有改变,所以可以执行CAS机制,但是变量已经发生了改变。
      • 如何解决:CAS默认使用的是变量的Value或者对象引用来做判断,在此基础上在添加一个参数,版本标志。JDK1.5之后,可以使用AtomicStampedReference.
    • 循环时间太长:

      • public final int getAndUpdate(IntUnaryOperator updateFunction) {
                int prev, next;
                do {
                    prev = get();
                    next = updateFunction.applyAsInt(prev);
                } while (!compareAndSet(prev, next));
                return prev;
            }
        
      • 以上代码是java.util.concurrent.atomic.AtomicInteger下面的一个方法,可以看到,CAS机制在while循环中一直执行,直到成功为止,这可能会出现自旋时间太长的问题。

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

      • 当多个变量都需要进行CAS保证其原子性的相关操作时,CAS无能为力。
      • 在JDK1.5之后,Java提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里面,从而对这个对象使用CAS机制。
  • AtomicInteger底层实现?

    • CAS机制保证数值相关操作的原子性,CAS操作由Unsafe类提供。
    • volatile关键字保证当前数值一旦被修改,则立即对其他线程可见。
  • 请阐述你对Unsafe类的理解?

    • Unsafe是位于sun.misc包下的类,主要提供一些低级别、不安全的操作方法,比如直接访问系统内存资源、更改变量的内存地址等,这些方法可以提高java运行的效率,增强java语言的底层操作能力。具体可以看上面的脑图。
  • AtomicStampedReference是怎么解决ABA的?

    • 在原有的把对象引用的基础上,增加另外一个标志——变量的版本标志,在做CAS操作时,必须两个值同时保持不变,才可以进行操作。
    • 构造包含两个变量reference和stamp的Pair对象,reference维护对象引用,stamp维护变量的版本标志信息
  • java中还有哪些类可以解决ABA的问题?

ger底层实现?**

  • CAS机制保证数值相关操作的原子性,CAS操作由Unsafe类提供。

  • volatile关键字保证当前数值一旦被修改,则立即对其他线程可见。

  • 请阐述你对Unsafe类的理解?

    • Unsafe是位于sun.misc包下的类,主要提供一些低级别、不安全的操作方法,比如直接访问系统内存资源、更改变量的内存地址等,这些方法可以提高java运行的效率,增强java语言的底层操作能力。具体可以看上面的脑图。
  • AtomicStampedReference是怎么解决ABA的?

    • 在原有的把对象引用的基础上,增加另外一个标志——变量的版本标志,在做CAS操作时,必须两个值同时保持不变,才可以进行操作。
    • 构造包含两个变量reference和stamp的Pair对象,reference维护对象引用,stamp维护变量的版本标志信息
  • java中还有哪些类可以解决ABA的问题?

    • AtomicMarkableReference
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值