Java并发基础 - CAS(Compare and swap)

1. Java中的原子操作

在java中,下列操作是原子操作:

  • all assignments of primitive types except for long and double,除long和double的原始类型赋值
  • all assignments of references,引用类型赋值
  • all operations of java.concurrent.Atomic* classes,Atomic原子类
  • all assignments to volatile longs and doubles,加了volatile的long和double类型的赋值

2. CAS(比较与交换,Compare and swap)

是一种有名的无锁算法。CAS 算法的过程是这样:它包含3个参数 CAS(V,E,N)。V(Value) 表示要更新的变量(内存值),E(Expect) 表示预期值(旧的),N 表示新值。当且仅当V值等于E值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

2.1 CAS的开销

前面说过了,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。

3. sun.misc.Unsafe

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

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

其他方法可以参考美团的此篇文章

3.1 AtomicInteger 源码解析

使用方式

//初始化,无参构造函数
AtomicInteger atomicCount = new AtomicInteger();
//或有参构造函数
AtomicInteger atomicCount = new AtomicInteger(1);

//调用具体方法
atomicCount.getAndIncrement();//返回当前值并+1
atomicCount.getAndAdd(2);//返回当前值并+2
atomicCount.incrementAndGet();//+1后并返回更新后的值
···省略

初始化

构造函数比较简单,没有多余的逻辑,直接看相关的成员变量和静态代码块:

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    //初始化的时候通过静态代码块给valueOffset赋值
    static {
        try {
            // valueOffset是value这个成员变量在内存中的地址,便于后续通过内存地址直接进行操作。
            // valueOffset 其实就是用来定位 value,后续 Unsafe 类可以通过内存地址直接对 value 进行操作。
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // value用来存储实际值
    private volatile int value;

了解了初始化逻辑,再来举个栗子看看getAndIncrement方法的实现:

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

    // Unsafe类的getAndAddInt方法
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // native方法,通过对象 var1 和成员变量相对于对象的内存偏移量 var2 来直接从内存地址中获取成员变量的值
            var5 = this.getIntVolatile(var1, var2);
            // native方法,CAS逻辑,通过对象 var1 和成员变量的内存偏移量 var2 来定位内存地址。
            // 如果内存中的数值是var5,则返回true(跳出do...while),将当前地址的值更新为var5+var4;如果不是var5,则返回false,继续循环
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

可以清晰的看出getAndAddInt方法的逻辑为:

  1. 根据对象var1和内存偏移地址var2来定位内存地址,获取当前地址值
  2. 循环通过 CAS 操作更新当前地址值直到更新成功
  3. 返回旧值

3.2 AtomicInteger线程安全测试

public class AtomicIntegerTest {
    private static final int THREAD_COUNTS = 20;
    //测试Integer
    private static Integer count = 0;
    //此处若是去掉synchronized同步,由于count++不是原子操作,会得出错误的结果
    private synchronized static void increment() {
        count++;
    }

    //测试AtomicInteger
    private static AtomicInteger atomicCount = new AtomicInteger(0);

    private static void atomicIncrement() {
        atomicCount.getAndIncrement();
    }


    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[THREAD_COUNTS];
        for (int i = 0; i < THREAD_COUNTS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                    atomicIncrement();
                }
            });
            threads[i].start();
        }

        //注意:idea中run启动时,还有一个线程Thread[Monitor Ctrl-Break,5,main];所以要>2;debug启动时正常。

        //返回活动线程的当前线程的线程组中的数量。
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        Thread.currentThread().getThreadGroup().list();

        //如果线程安全,则会输出20*1000的数值,即线程*循环次数
        System.out.println("count---" + count);
        System.out.println("atomicCount---" + atomicCount.get());

    }
}

4. CAS的缺点

4.1 自旋消耗资源:循环时间长,开销很大。

从上面的源码可以看到在getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。可参考JDK8新增的LongAddr,和ConcurrentHashMap采用的方法。

4.2 多变量共享一致性问题

CAS操作是针对一个变量的,如果对多个变量操作,1. 可以加锁来解决。2 .封装成对象类解决。

4.3 ABA问题

CAS检查的时候发现值没有改变,但是实质上它可能已经发生了改变 。可能会造成数据的缺失。

解决方法:同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference。

具体的示例和验证,之后在写 :)

发布了61 篇原创文章 · 获赞 44 · 访问量 10万+
展开阅读全文

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

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览