CAS了解及CAS容易发生的问题

CAS(Compare and Swap)比较和交换,是java在处理并发问题时,使用最多的一种方式,简单说就是,指定一个对象V,给出他的期望值,及需要修改的值,如果期望值等同于内存中的值,那么就把这个对象修改成我们想要改变的值,否则修改失败。

CAS使用最佳实践

    先看下我们下面的场景:

public class Case {
 
    public volatile int n;
 
    public void add() {
        n++;
    }
}

int 类型的对象n, 我们使用volatile修饰,来保证了内存可见性,但是当多线程情况下,我们调用add()方法,由于该方法并不是线程安全的,并且n++这个操作并不是原子操作,(其实是三步1、从主内存获取n的值,2、n的值+1,3、将n的值写会至主内存)在以上三步中,如果多线程同时访问,那么就会造成n的值小于预期的值。我们怎么解决上面这个情况呢

    最常见的办法就是 在add方法上添加synchronized关键字,当然可以保证多线程的安全性,但是并不是很推荐(synchronized是重量级锁,获取锁和释放锁,需要进行内核态和用户态之间的切换,后面会讨论synchronized和cas性能问题)

    还有一种解决办法就是使用java中提供的CAS操作,在CAS操作当中,这个步骤是原子操作,不会被其他的线程访问,同时也不需要加锁。性能上可能会更加有优势,我们来看下java中提供对Integer值得增加,修改的原子操作类AtomicInteger

AtomicInteger底层是通过unsafe类来操作数据的,其内部维护了一个Integer类型的属性value
 

static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

1、这段代码时获取到value这个属性相对于AtomicInteger对象的内存偏移量,方便后续直接通过unsafe来修改内存当中的值

对于上面我们的n++操作,对应到AtomicInteger类当中

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




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

通过对象和对象中属性的偏移量,来计算出当前偏移量下,存储的旧的数值,然后在旧值上增加新值,这一步需要注意的是,通过do-while这种方式,直到修改成功

最终调用的就是unsafe当中的compareAndSwapInt

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

其参数就是当前对象,var2:属性相对于当前对象的偏移量,var4 :期待值,var5:修改的新值

CAS操作容易出现的问题:

1、ABA问题,线程1 从内存当中获取到变量V的值是A,线程2也从内存当中获取到V的值A,然后线程2将V的值修改成B, 然后线程2又将变量V的值改成了A,这时候线程1 判断变量V的值也是A,然后就进行修改成功了。

这个时候可能就会有个疑问即使我中间变化了,可我最终还是把值给改成我所期待的值,并不会有影响,这种想法只是在修改一个Integer类型时,可能中间修改过多少次并没有太大的影响,但是如果是链表的话,那么这个影响是很大的

比如单向链表,A-->B--->null, 如图

加入此时线程1 想要把栈顶A 替换为B, head.comareAndSet(A,B),还没有执行成功

此时线程2 进入,将链表当中A后面的元素成功设置成了C和D,

此时线程1 回来执行compareAndSwap操作,发现栈顶还是A ,然后执行成功,但是此时链表变成了如下

 

此时栈顶中只有一个元素B,这种情况下就会把C、D元素丢掉了

使用AtomicStampedReference来解决ABA问题

    AtomicStampedReference对象当中维护了内部类,在原有基础上维护了一个对象,及一个int类型的值(可以理解为版本号),在每次进行对比修改时,都会先判断要修改的值,和内存中的值是否相同,以及版本号是否相同
 


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

以上是其内部类,我们在创建一个对象时,同时会出传递一个时间戳或者版本号,在每次进行修改时,都会修改原来的时间戳或者版本号

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

 在compareAndSet时,会传入期望值,新值,期望版本号,新的版本号,并且使用unsafe的方式修改object对象

private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

CAS和Synchronized对比

    在jdk1.7中,AtomicInteger的getAndIncrement方法实现如下

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
}

这种情况下,CAS操作适合于竞争不激烈的情况下,通过硬件来完成原子操作,不需要加锁,也需要进行用户态和核心态的一个切换,在竞争理解的情况下,线程进入自旋更新操作中,而synchronized是通过使线程阻塞,来阻止对值得修改,这时候我们查看cpu会发现,通过CAS操作,当线程达到一定数量之后,更加耗时,同时CPU占有率更高,而synchronized方式,他的CPU占有率不会增加,


但是在JDK 1.8当中,AtomicInteger进行了优化
 

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

经过测试,发现CAS耗时基本上和synchronized差不多,总体上CAS是低于synchronized的耗时


public class CASDemo {
    private final int THREAD_NUM = 800;
    private final int MAX_VALUE = 20000000;
    private AtomicInteger casI = new AtomicInteger(0);
    private int syncI = 0;
    private Object obj = new Object();
 
    public static void main(String[] args) throws Exception {
        CASDemo casDemo = new CASDemo();
        casDemo.casAdd();
        casDemo.syncAdd();
    }
 
    public void casAdd() throws Exception {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (casI.get() < MAX_VALUE) {
                        casI.getAndIncrement();
                    }
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i].join();
        }
        System.out.println("cas costs time :" + (System.currentTimeMillis() - begin));
    }
 
    public void syncAdd() throws Exception {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (syncI < MAX_VALUE) {
                        synchronized (obj) {
                            ++syncI;
                        }
                    }
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i].join();
        }
        System.out.println("synchronized cost:" + (System.currentTimeMillis() - begin));
    }
 
 
}
发布了393 篇原创文章 · 获赞 41 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/qq_32440951/article/details/89554654
Cas