CAS

文章目录

  • 1. CAS
    • 1.1 count++ 问题
    • 1.2 自己实现 CAS
    • 1.3 JDK 中的 CAS
    • 1.4 存在的问题
  • 2. LongAdder
    • 2.0 参考文献
    • 2.1 AtomicLong 存在的问题
    • 2.2 LongAdder 解决以及带来的问题
    • 2.3 AtomicLong VS LongAdder
    • 2.4 源码阅读

1. CAS

1.1 count++ 问题

public class Test {
    private int count =0;
    void add(){
        count++;
    }
}
//使用 javap -c (-c 就是对代码进行反汇编)
$ javap -c Test.class
Compiled from "Test.java"
public class tree.Test {
  public tree.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field count:I
       9: return

  void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field count:I
      10: return
}
/*
    getfield:从内存中获取变量count的值
    iadd:将count+1
    putfield:将加1后的结果赋值给count变量
    count++对应着三步操作,不能保证原子操作.
    	线程 A 	      线程 B
       内存取值
         +1 
                     内存取值,和A线程取到的值相同
                     	+1
      赋值给 count     赋值给 count
        最终就导致了少计算一次 count 值
*/

1.2 自己实现 CAS

public class Demo01 {

    volatile static int count = 0;
	
    /*
    	count++ 的三个操作:
    		获取 count 值 A
    		conut+1 得到 B = A+1
    		将加完的值 B 赋给 count
		现在将第三步加锁:
			1)获取锁
			2)获取 count 的最新值 LV
			3)判断 LV 是否等于 A,如果相等,则将 B 的值赋给 count,并返回 true, 否则返回 true
			4)释放锁
    */
    public static void request() throws InterruptedException {

        TimeUnit.MILLISECONDS.sleep(5);

        while (!compareAndSwap(getCount(), getCount() + 1)) {
        }
    }
    
    // expect:期望值 newCount:就是新的值
    public static synchronized boolean compareAndSwap(int expect, int newCount) {
        if (expect == getCount()) {
            count = newCount;
            return true;
        } else {
            return false;
        }
    }

    public static int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    request();
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }).start();
        }

        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("时间:"+(endTime-startTime));//时间:51
        System.out.println(count);//100
    }

}

1.3 JDK 中的 CAS

//UnSafe.class
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);

/*
	var1:表示要操作的对象
	var2:要操作对象中属性地址的偏移量
	var4:需要修改数据的期望值
	var5:表示需要修改为得新值
*/
//先获取在加(类似i++,先使用后自增)
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //public native int getIntVolatile(Object var1, long var2);
        //从内存中获取 var1,var2 地址对应变量的值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

CAS_第1张图片

1.4 存在的问题

  • 自旋太久

    CAS_第2张图片

  • ABA问题

    CAS_第3张图片

    解决方案:

    CAS_第4张图片

    • 如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
    • Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
    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;
    }
    
  • 只能保证一个共享变量的原子操作

    • 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

2. LongAdder

2.0 参考文献

  • 面试官问我LongAdder,我惊了…

2.1 AtomicLong 存在的问题

  • 阿里巴巴开发手册: 执行 count++ 操作,使用 LongAdder 对象比 AtomicLong 性能更好(减少乐观锁重试次数 即 CAS)

  • AtomicLong 高并发场景下性能急剧下降的原因?

    • 当我们在进行计数统计的时,通常会使用AtomicLong来实现。AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。

    • AtomicLong 计数方法最终调用的是 Unsafe 里的 public final int getAndAddInt(Object var1, long var2, int var4) 方法, getAndAddInt 采用 CAS + 自旋 操作更新 新的值.

    • 性能瓶颈:

      CAS_第5张图片

2.2 LongAdder 解决以及带来的问题

  • 传统的原子锁AtomicLong/AtomicInteger虽然也可以处理大量并发情况下的计数器,但是由于使用了自旋等待,当存在大量竞争时,会存在大量自旋等待,而导致CPU浪费,而有效计算很少,降低了计算效率

  • LongAdder是根据ConcurrentHashMap这类为并发设计的类的基本原理——锁分段,通过维护一个计数数组cells来保存多个计数副本,每个线程只对自己的副本进行操作,最后汇总来得到最终结果。

CAS_第6张图片

  • AtomicLong 和 LongAdder 原理分析/设计思想

    • AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点数据,也就是N个线程竞争一个热点。

    • LongAdder的基本思路就是==分散热点,将value值的新增操作分散到一个数组中==,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个value值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。

    • LongAdder有一个全局变量volatile long base值,当并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率

    • CAS_第7张图片

    • 要想获取真正的 long 值, 需要调用 sum 方法, sum 方法将 每个cell中的值求和 + base 值,然后返回. 存在的问题: 调用 sum 方法时,有 add 操作,会导致结果不准确, 但是最终还是正确的, 即 最终一致性 (CopyOnWriteArrayList 也是最终一致性)

    • @sun.misc.Contended 这个注解是用来消除伪共享的.

      • 当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行(位处理器,一个缓存行 64字节, long 是 8字节),就会无意中影响彼此的性能,这就是伪共享。
      • 解决方法: 1)每两个变量之间加七个 long 类型 2)创建自己的 long 类型,而不是用原生的;3) 使用这个主注解Contended
      • 其他使用到 伪共享的类 ConcurrentHashMap
      //什么是伪共享? -- 好文章: https://www.cnblogs.com/tong-yuan/p/FalseSharing.html
      abstract class Striped64 extends Number{
      	@sun.misc.Contended static final class Cell {
              volatile long value;
              Cell(long x) { value = x; }
              final boolean cas(long cmp, long val) {
                  return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
              }
          }
      }
      //1)每两个变量之间加七个 long 类型
      class Pointer {
          volatile long x;
          long p1, p2, p3, p4, p5, p6, p7;
          volatile long y;
      }
      //2)
      class MyLong {
          volatile long value;
          long p1, p2, p3, p4, p5, p6, p7;
      }
      //3)
      @sun.misc.Contended
      class MyLong {
          volatile long value;
      }
      

2.3 AtomicLong VS LongAdder

CAS_第8张图片

  • AtomicLong 计数 ,强一致性.
  • LongAdder 在求 sum 时,出现 add 操作, sum 结果不准确, 是最终一致性

2.4 源码阅读

  • public class LongAdder extends Striped64 implements Serializable {
        public void increment() {
            add(1L);
        }
        public void decrement() {
            add(-1L);
        }
        
        public void add(long x) {
            Cell[] as; long b, v; int m; Cell a;
            if ((as = cells) != null || !casBase(b = base, b + x)) {
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[getProbe() & m]) == null ||
                    !(uncontended = a.cas(v = a.value, v + x)))
                    longAccumulate(x, null, uncontended);
            }
        }
    }
    
    abstract class Striped64 extends Number {
    	final void longAccumulate(long x, LongBinaryOperator fn,
                                  boolean wasUncontended){
            ....
        }
    }
    
  • longAccumulate 的流程图

    CAS_第9张图片

  • add 流程图

    CAS_第10张图片

  • 简单的讲:

    add 过程:

    • 首先对 base 进行修改
    • 修改失败,对 cells 进行初始化操作
    • 初始化完成, 判断当前线程所在 cell 是否为 null
    • 如果 为 null,则创建 ,不为 null,则进行修改 cell 的 value
    • 修改失败的话,准备对 cells 数组进行扩容,不是立马扩容的.

    扩容的条件:(三个条件同时满足)

    • 当前线程的 cell 第一次修改失败
    • “rehash” 后,换一个新的 cell 后还是失败
    • 且能够获取锁,对当前 cells 进行操作, 才进行扩容

CAS_第11张图片

你可能感兴趣的:(Java并发)