当前位置:   article > 正文

CAS了解及CAS容易发生的问题_cas容易踩的坑

cas容易踩的坑

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

CAS使用最佳实践

    先看下我们下面的场景:

  1. public class Case {
  2. public volatile int n;
  3. public void add() {
  4. n++;
  5. }
  6. }

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

  1. static {
  2. try {
  3. valueOffset = unsafe.objectFieldOffset
  4. (AtomicInteger.class.getDeclaredField("value"));
  5. } catch (Exception ex) { throw new Error(ex); }
  6. }

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

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

  1. public final int getAndIncrement() {
  2. return unsafe.getAndAddInt(this, valueOffset, 1);
  3. }
  1. public final int getAndAddInt(Object var1, long var2, int var4) {
  2. int var5;
  3. do {
  4. var5 = this.getIntVolatile(var1, var2);
  5. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  6. return var5;
  7. }

通过对象和对象中属性的偏移量,来计算出当前偏移量下,存储的旧的数值,然后在旧值上增加新值,这一步需要注意的是,通过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类型的值(可以理解为版本号),在每次进行对比修改时,都会先判断要修改的值,和内存中的值是否相同,以及版本号是否相同

  1. private static class Pair<T> {
  2. final T reference;
  3. final int stamp;
  4. private Pair(T reference, int stamp) {
  5. this.reference = reference;
  6. this.stamp = stamp;
  7. }
  8. static <T> Pair<T> of(T reference, int stamp) {
  9. return new Pair<T>(reference, stamp);
  10. }
  11. }

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

  1. public boolean compareAndSet(V expectedReference,
  2. V newReference,
  3. int expectedStamp,
  4. int newStamp) {
  5. Pair<V> current = pair;
  6. return
  7. expectedReference == current.reference &&
  8. expectedStamp == current.stamp &&
  9. ((newReference == current.reference &&
  10. newStamp == current.stamp) ||
  11. casPair(current, Pair.of(newReference, newStamp)));
  12. }

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

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

CAS和Synchronized对比

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

  1. public final int getAndIncrement() {
  2. for (;;) {
  3. int current = get();
  4. int next = current + 1;
  5. if (compareAndSet(current, next))
  6. return current;
  7. }
  8. }

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


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

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

 

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

 

  1. public class CASDemo {
  2. private final int THREAD_NUM = 800;
  3. private final int MAX_VALUE = 20000000;
  4. private AtomicInteger casI = new AtomicInteger(0);
  5. private int syncI = 0;
  6. private Object obj = new Object();
  7. public static void main(String[] args) throws Exception {
  8. CASDemo casDemo = new CASDemo();
  9. casDemo.casAdd();
  10. casDemo.syncAdd();
  11. }
  12. public void casAdd() throws Exception {
  13. long begin = System.currentTimeMillis();
  14. Thread[] threads = new Thread[THREAD_NUM];
  15. for (int i = 0; i < THREAD_NUM; i++) {
  16. threads[i] = new Thread(new Runnable() {
  17. @Override
  18. public void run() {
  19. while (casI.get() < MAX_VALUE) {
  20. casI.getAndIncrement();
  21. }
  22. }
  23. });
  24. threads[i].start();
  25. }
  26. for (int i = 0; i < THREAD_NUM; i++) {
  27. threads[i].join();
  28. }
  29. System.out.println("cas costs time :" + (System.currentTimeMillis() - begin));
  30. }
  31. public void syncAdd() throws Exception {
  32. long begin = System.currentTimeMillis();
  33. Thread[] threads = new Thread[THREAD_NUM];
  34. for (int i = 0; i < THREAD_NUM; i++) {
  35. threads[i] = new Thread(new Runnable() {
  36. @Override
  37. public void run() {
  38. while (syncI < MAX_VALUE) {
  39. synchronized (obj) {
  40. ++syncI;
  41. }
  42. }
  43. }
  44. });
  45. threads[i].start();
  46. }
  47. for (int i = 0; i < THREAD_NUM; i++) {
  48. threads[i].join();
  49. }
  50. System.out.println("synchronized cost:" + (System.currentTimeMillis() - begin));
  51. }
  52. }

 

 

参考:面试必问的CAS,要多了解

 

 

          用AtomicStampedReference解决ABA问题

          Java并发编程总结2——慎用CAS

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/303194
推荐阅读
相关标签
  

闽ICP备14008679号