Java并发——CAS原子操作、java.util.concurrent.atomic原子变量类源码解析

版权声明:个人博客:blog.suyeq.com 欢迎访问 https://blog.csdn.net/hackersuye/article/details/84929011

    比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。 ---------维基百科

    CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS操作说明了"调用者认为位置V的值是A;如果确实如此,则将B值替换A值;否则,不做处理,告诉调用者原有的值是多少即可"。

    Java中的锁机制都是独占锁,当持有锁的一方在使用时,其他需要锁的操作只能陷入等待,白白的浪费资源,所以锁机制被称为悲观锁。相较于用锁来实现同步的技术,CAS实现的同步是一种乐观的实现,之所以是一种乐观的实现,是因为它是无锁且非阻塞的。关于无锁,CAS原子操作是由处理器支持的,不需要由使用者自己实现,只需要调用即可,也就是说我们使用CAS原子操作是调用处理器的底层命令来实现同步操作。关于非阻塞,在CAS原子操作中,并不会因为一个线程在执行该操作时,另外一个线程会陷入等待,它们两者会同时进行。

    为了更好的说明CAS原子操作以及它的非阻塞属性,我们来描述一个场景,即在多线程的环境下对一个值进行自增操作。假设有两个线程在运行,线程A从内存中取出值与它所期望得到的值进行比较,如果相等,则对其进行自增操作。线程B在线程A运行时同样运行,也从内存中取出值进行比较,当发现值与预期的值不一样时(多处理器中的CAS指令保证了不会出现可见性、有序性、原子性问题),线程B会可能采取两种操作,一种是重试操作,继续对值进行CAS自增操作,这种策略保证每个线程执行自增都能完成。另外一种是放弃此次操作,保证多个线程进行自增只能有一个线程完成自增操作。当某个CAS原子操作失败时,该线程不会陷入等待,而是放弃这次操作或者进行重试,当CAS原子操作放弃时,意味着此次操作已经由另外的线程实现了

    下面利用内置锁来模拟CAS操作,切记,CAS是无锁实现的,因为Java程序并不能直接调用底层来实现CAS操作,在这里只是做个模拟:

public class Caculate {
    private int value;
    public synchronized int getValue(){
        return value;
    }
    public synchronized int compareAndSwap(int expectValue,int newValue){
        int oldValue=value;
        if (oldValue==expectValue){
            value=newValue;
        }
        return oldValue;
    }
    //采用第一种策略,每个线程自增总能成功
    public int increment(){
        int temp;
        do {
            temp=getValue();
        }while (temp!=compareAndSwap(temp,temp+1));
        return temp+1;
    }
}

    CAS原子操作的优点很明显,无锁且非阻塞,但是它的复杂程度远高于锁机制,出于这点,围绕CAS原子操作构建的多线程算法的难度大大提升。而且相较于锁机制,当程序复杂起来的时候,它带给开发者调试以及测试的可读性很差。从它的操作来看,循环增加了它的开销,但事实上在竞争程度不高的程序中,它的性能远远超过了锁机制,但是在竞争程度极高的程序中,它的性能也就比锁机制低一些,但是在现实中,CAS原子操作往往比锁机制更高效。另外一个问题是ABA问题:

    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 java.util.atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    java.util.atomic包里面的类称之为原子变量类,是Java实现了CAS原子操作的类,它们的作用如下:

原子变量类 作用
AtomicBoolean 原子方式更新的 boolean 值。
AtomicInteger 原子方式更新的 int 值
AtomicIntegerArray 原子方式更新其元素的 int 数组
AtomicIntegerFieldUpdater 对指定类的指定 volatile int 字段进行原子更新
AtomicLong 原子方式更新的 long 值
AtomicReference 原子方式更新的对象引用

    找最常用的AtomicInteger类来解析下它的源码,从它的源码中可以看到,它的底层实现都交给了Unsafe类来实现,比如自增操作:

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    Unsafe类是实现所有CAS原子操作的基础,它将原子操作都交给JVM执行,比如上面的getAndAddInt方法:

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 native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    原子变量类是一种更好的volatile变量,因为它保证了原子性,而valance变量则不能。

展开阅读全文

没有更多推荐了,返回首页