文章目录
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."<init>":()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;
}
1.4 存在的问题
-
自旋太久
-
ABA问题
解决方案:
- 如果内存地址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 参考文献
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 + 自旋 操作更新 新的值. -
性能瓶颈:
-
2.2 LongAdder 解决以及带来的问题
-
传统的原子锁AtomicLong/AtomicInteger虽然也可以处理大量并发情况下的计数器,但是由于使用了自旋等待,当存在大量竞争时,会存在大量自旋等待,而导致CPU浪费,而有效计算很少,降低了计算效率。
-
LongAdder是根据ConcurrentHashMap这类为并发设计的类的基本原理——锁分段,通过维护一个计数数组cells来保存多个计数副本,每个线程只对自己的副本进行操作,最后汇总来得到最终结果。
-
AtomicLong 和 LongAdder 原理分析/设计思想
-
AtomicLong
中有个内部变量value
保存着实际的long
值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value
变量其实是一个热点数据,也就是N个线程竞争一个热点。 -
LongAdder
的基本思路就是==分散热点,将value
值的新增操作分散到一个数组中==,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个value
值进行CAS
操作,这样热点就被分散了,冲突的概率就小很多。 -
LongAdder
有一个全局变量volatile long base
值,当并发不高的情况下都是通过CAS
来直接操作base
值,如果CAS
失败,则针对LongAdder
中的Cell[]
数组中的Cell
进行CA
S操作,减少失败的概率。 -
要想获取真正的 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
- 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 的流程图
-
add 流程图
-
简单的讲:
add 过程:
- 首先对 base 进行修改
- 修改失败,对 cells 进行初始化操作
- 初始化完成, 判断当前线程所在 cell 是否为 null
- 如果 为 null,则创建 ,不为 null,则进行修改 cell 的 value
- 修改失败的话,准备对 cells 数组进行扩容,不是立马扩容的.
扩容的条件:(三个条件同时满足)
- 当前线程的 cell 第一次修改失败
- “rehash” 后,换一个新的 cell 后还是失败
- 且能够获取锁,对当前 cells 进行操作, 才进行扩容